They referred to project documents that are not part of this repository. Comments only; PriceToPips is now documented as not rounding, which is what it does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
330 lines
14 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|