StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| Symbols.mqh|
|
| | | //| Copyright 2026, bandersoft |
|
| | | //| https://www.mql5.com/en/users/fats |
|
| | | //+------------------------------------------------------------------+
|
| | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| Broker symbol-name resolution. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| Read-only: sends no trade requests. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
| | | //| "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
|
2026-10-16 21:48:01 +03:00 | | | //--- symbol the caller did not select would take away another program's
|
| | | //--- quotes
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | 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
|
| | | //+------------------------------------------------------------------+
|