//+------------------------------------------------------------------+ //| Symbols.mqh| //| Copyright 2026, bandersoft | //| https://www.mql5.com/en/users/fats | //+------------------------------------------------------------------+ //| | //| Broker symbol-name resolution. | //| | //| Read-only: sends no trade requests. | //| | //| "EURUSD" is not a symbol name, it is a hope. Brokers ship it as | //| EURUSD.m, EURUSDpro, EURUSD_raw, EURUSD.ecn, m.EURUSD, EURUSD# | //| and worse. An EA that hardcodes the bare name works on the | //| developer's broker and silently does nothing on the client's. | //| | //| Resolve() scans SymbolsTotal(false) - every symbol the server | //| offers, not just Market Watch - and caches what it finds. | //| | //+------------------------------------------------------------------+ #ifndef STOPFIRST_SYMBOLS_MQH #define STOPFIRST_SYMBOLS_MQH #include "Defs.mqh" //--- resolution cache. Scanning every symbol is not free and the //--- answer cannot change while the terminal is running. string g_symbolQueries[]; string g_symbolResults[]; //+------------------------------------------------------------------+ //| Strip everything but letters and digits, and upper-case it | //| | //| Turns "EURUSD.m", "eurusd_raw" and "m.EURUSD" into comparable | //| text so the separator a broker chose stops mattering. | //| | //| Returns: the normalised name. | //+------------------------------------------------------------------+ string NormalizeSymbolName(const string name) { string out = ""; int length = StringLen(name); for(int i = 0; i < length; i++) { ushort ch = StringGetCharacter(name, i); //--- the cast is explicit: ch - 32 promotes to int, and //--- ShortToString takes a ushort if(ch >= 'a' && ch <= 'z') { out += ShortToString((ushort)(ch - 32)); continue; } if(ch >= 'A' && ch <= 'Z') { out += ShortToString(ch); continue; } if(ch >= '0' && ch <= '9') { out += ShortToString(ch); continue; } //--- '.', '_', '-', '#', spaces and anything else are dropped } return out; } //+------------------------------------------------------------------+ //| Does one separator-delimited part of a broker name equal a base? | //| | //| "m.EURUSD", "EURUSD.m", "#EURUSD" and "EURUSD_raw" each contain | //| EURUSD as a whole part. Normalising the whole name does not find | //| them: it keeps the 'm' and gives MEURUSD or EURUSDM (found in | //| review 2026-10-14 - the doc said these resolved; they did not). | //| | //| Parameters: | //| wanted - already normalised (upper case, letters and digits) | //| | //| Returns: true if any part, upper-cased, equals wanted. | //+------------------------------------------------------------------+ bool SymbolNameHasPart(const string candidate, const string wanted) { string part = ""; int length = StringLen(candidate); for(int i = 0; i <= length; i++) { ushort ch = (i < length) ? StringGetCharacter(candidate, i) : 0; bool alnum = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'); if(alnum) { part += ShortToString(ch); continue; } if(part != "") { StringToUpper(part); if(part == wanted) return true; part = ""; } } return false; } //+------------------------------------------------------------------+ //| Do a candidate's own currencies name a different pair? | //| | //| A prefix match cannot tell "EURUSDpro" (EUR/USD) from "EURUSDT" | //| (EUR against Tether); the symbol's currencies can. Only applied | //| when the wanted name looks like a six-letter pair and the | //| candidate reports two distinct currencies - indices and CFDs that | //| report one currency twice are left to the name. | //| | //| Returns: true when the candidate is provably another pair. | //+------------------------------------------------------------------+ bool SymbolPairContradicts(const string candidate, const string wanted) { if(StringLen(wanted) != 6) return false; string base = SymbolInfoString(candidate, SYMBOL_CURRENCY_BASE); string profit = SymbolInfoString(candidate, SYMBOL_CURRENCY_PROFIT); if(base == "" || profit == "" || base == profit) return false; string pair = base + profit; StringToUpper(pair); return pair != wanted; } int SymbolCacheIndex(const string query) { int total = ArraySize(g_symbolQueries); for(int i = 0; i < total; i++) if(g_symbolQueries[i] == query) return i; return -1; } void CacheSymbolResult(const string query, const string result) { int index = SymbolCacheIndex(query); if(index >= 0) { g_symbolResults[index] = result; return; } int size = ArraySize(g_symbolQueries); ArrayResize(g_symbolQueries, size + 1); ArrayResize(g_symbolResults, size + 1); g_symbolQueries[size] = query; g_symbolResults[size] = result; } //+------------------------------------------------------------------+ //| Forget every cached resolution | //| Returns: nothing. | //+------------------------------------------------------------------+ void ClearSymbolCache(void) { ArrayResize(g_symbolQueries, 0); ArrayResize(g_symbolResults, 0); } //+------------------------------------------------------------------+ //| Make sure a symbol is in Market Watch | //| | //| A symbol the server offers but which is not selected returns no | //| quote and no history. SymbolSelect fixes that, and is harmless | //| when it is already selected. | //| | //| Returns: true if the symbol is selected afterwards. | //+------------------------------------------------------------------+ bool EnsureSymbolSelected(const string symbol) { if(symbol == "") return false; if(SymbolInfoInteger(symbol, SYMBOL_SELECT)) return true; return SymbolSelect(symbol, true); } //+------------------------------------------------------------------+ //| Find the broker's actual name for a base symbol | //| | //| Parameters: | //| baseName - the plain name, e.g. "EURUSD" or "XAUUSD" | //| selectInMarketWatch - add the match to Market Watch | //| | //| Matching order, most to least certain: | //| 1. the exact name, if the server has it | //| 2. a symbol with the base as one separator-delimited part: | //| "EURUSD.m", "m.EURUSD", "#EURUSD", "EURUSD_raw" | //| 3. a symbol whose normalised name STARTS with the base and adds | //| 1 to 4 characters ("EURUSDpro", "EURUSDm"), the SHORTEST | //| such name winning, and none whose own currencies name a | //| different pair | //| | //| Step 3 is deliberately tight. A loose prefix match on a crypto | //| broker turns EURUSD into EURUSDT, a different instrument. Until | //| 2026-10-14 it took the FIRST hit in server order and let EURUSDT | //| through; the currency check now refuses it. | //| | //| Returns: the broker symbol name, or "" if nothing matched. | //+------------------------------------------------------------------+ string ResolveSymbol(const string baseName, const bool selectInMarketWatch = true) { if(baseName == "") return ""; int cached = SymbolCacheIndex(baseName); if(cached >= 0) { string hit = g_symbolResults[cached]; if(hit != "" && selectInMarketWatch) EnsureSymbolSelected(hit); return hit; } //--- 1. exact. Leave Market Watch as it was found: deselecting a //--- symbol the caller did not select would take away another program's //--- quotes bool wasSelected = (bool)SymbolInfoInteger(baseName, SYMBOL_SELECT); if(wasSelected || SymbolSelect(baseName, true)) { CacheSymbolResult(baseName, baseName); if(!selectInMarketWatch && !wasSelected) SymbolSelect(baseName, false); return baseName; } string wanted = NormalizeSymbolName(baseName); if(wanted == "") { CacheSymbolResult(baseName, ""); return ""; } //--- false means every symbol the server offers, not just the ones //--- currently in Market Watch int total = SymbolsTotal(false); string prefixMatch = ""; int prefixExtra = 1000; for(int i = 0; i < total; i++) { string candidate = SymbolName(i, false); if(candidate == "") continue; //--- 2. the base as a whole part of the name if(SymbolNameHasPart(candidate, wanted)) { CacheSymbolResult(baseName, candidate); if(selectInMarketWatch) EnsureSymbolSelected(candidate); return candidate; } //--- 3. remember the TIGHTEST prefix match, but keep looking for //--- a step-2 one string normalized = NormalizeSymbolName(candidate); if(StringFind(normalized, wanted) == 0) { int extra = StringLen(normalized) - StringLen(wanted); if(extra > 0 && extra <= 4 && extra < prefixExtra && !SymbolPairContradicts(candidate, wanted)) { prefixMatch = candidate; prefixExtra = extra; } } } if(prefixMatch != "") { CacheSymbolResult(baseName, prefixMatch); if(selectInMarketWatch) EnsureSymbolSelected(prefixMatch); return prefixMatch; } //--- cache the miss too: a failed scan is as expensive as a hit CacheSymbolResult(baseName, ""); return ""; } //+------------------------------------------------------------------+ //| Does the broker offer this symbol under some name? | //| Returns: true if ResolveSymbol finds one. | //+------------------------------------------------------------------+ bool SymbolExists(const string baseName) { return ResolveSymbol(baseName, false) != ""; } //+------------------------------------------------------------------+ //| The suffix a broker appends, derived from the current chart | //| | //| Useful for building a sibling name directly. On a chart of | //| "EURUSD.m" this returns ".m", so "GBPUSD" + suffix is a good | //| first guess - though ResolveSymbol is more reliable, because | //| some brokers use a prefix instead. | //| | //| Returns: the trailing characters after the 6-letter pair name, | //| or "" if the chart symbol does not look like an FX pair. | //+------------------------------------------------------------------+ string DetectSymbolSuffix(const string reference = "") { string symbol = (reference == "") ? _Symbol : reference; //--- only meaningful for the 6-character pair convention if(StringLen(symbol) <= 6) return ""; string head = StringSubstr(symbol, 0, 6); if(NormalizeSymbolName(head) != head) return ""; // not six plain letters return StringSubstr(symbol, 6); } //+------------------------------------------------------------------+ //| Resolve several base names at once | //| | //| Parameters: | //| baseNames - what to look for | //| resolved - receives one entry per input, "" where not found | //| | //| Returns: how many were found. | //+------------------------------------------------------------------+ int ResolveSymbols(const string &baseNames[], string &resolved[], const bool selectInMarketWatch = true) { int total = ArraySize(baseNames); ArrayResize(resolved, total); int found = 0; for(int i = 0; i < total; i++) { resolved[i] = ResolveSymbol(baseNames[i], selectInMarketWatch); if(resolved[i] != "") found++; } return found; } #endif // STOPFIRST_SYMBOLS_MQH //+------------------------------------------------------------------+