StopFirst-Core/Include/StopFirst/Symbols.mqh

330 lines
14 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+