//+------------------------------------------------------------------+ //| UQ_Symbols.mqh | //| Universal Quant EA - symbol resolution and signal engine | //| | //| Deterministic volatility-normalised channel breakout: | //| long : completed close > highest high of the preceding | //| lookback window AND completed close > EMA | //| short : completed close < lowest low of the preceding | //| lookback window AND completed close < EMA | //| Only completed candles are used; the forming bar is never read | //| for confirmation. | //| | //| Resolution fails closed. An exact catalogue name is used when it | //| exists; a broker prefix/suffix variant is accepted only when the | //| broker convention is verified and exactly one eligible | //| instrument remains; anything else is reported as an error. No | //| arbitrary candidate is ever chosen. | //+------------------------------------------------------------------+ #ifndef UQ_SYMBOLS_MQH #define UQ_SYMBOLS_MQH #include "UQ_Defs.mqh" //+------------------------------------------------------------------+ //| Per-symbol runtime state | //+------------------------------------------------------------------+ struct UQSymbol { string requested; // token supplied by the operator string name; // resolved broker symbol bool resolved; int hEma; // EMA trend filter handle int hAtr; // ATR handle datetime lastBar; // last observed signal-bar open time datetime lastEntryBar; // signal bar already acted upon datetime lastLogTime; // throttling for repeated diagnostics string lastNote; }; //+------------------------------------------------------------------+ //| Split a catalogue name around a token: name = prefix+token+suffix | //| Returns false when the token is absent or the surrounding | //| extension is implausibly long for a broker prefix/suffix tag. | //+------------------------------------------------------------------+ bool UQSymbolExtension(const string name, const string token, string &extPrefix, string &extSuffix) { extPrefix = ""; extSuffix = ""; if(StringLen(name) <= 0 || StringLen(token) <= 0) return(false); string upperName = name; string upperToken = token; StringToUpper(upperName); StringToUpper(upperToken); const int pos = StringFind(upperName, upperToken); if(pos < 0) return(false); extPrefix = StringSubstr(name, 0, pos); extSuffix = StringSubstr(name, pos + StringLen(token)); if(StringLen(extPrefix) + StringLen(extSuffix) > UQ_SYMBOL_EXT_MAX_LEN) return(false); return(true); } //+------------------------------------------------------------------+ //| Standard pairs used to infer this broker's prefix/suffix layout. | //| The list is fixed so detection stays deterministic across runs. | //+------------------------------------------------------------------+ string UQProbeSymbol(const int index) { switch(index) { case 0: return("EURUSD"); case 1: return("GBPUSD"); case 2: return("USDJPY"); case 3: return("USDCHF"); case 4: return("AUDUSD"); case 5: return("USDCAD"); case 6: return("NZDUSD"); case 7: return("EURJPY"); case 8: return("EURGBP"); case 9: return("GBPJPY"); } return(""); } //+------------------------------------------------------------------+ //| Verify the broker symbol naming convention. | //| | //| A convention is only trusted when several independent standard | //| pairs agree on the same prefix/suffix, so a single broker name | //| that happens to contain a pair token can never define it. When | //| no convention is verified the tag stays empty and the resolver | //| falls back to an explicit ambiguity error instead of guessing. | //+------------------------------------------------------------------+ bool UQDetectBrokerTag(string &tagPrefix, string &tagSuffix) { tagPrefix = ""; tagSuffix = ""; string keys[]; int counts[]; int distinct = 0; int totalVote = 0; const int catalogue = SymbolsTotal(false); for(int probe = 0; probe < UQ_SYMBOL_PROBE_MAX; probe++) { const string token = UQProbeSymbol(probe); if(StringLen(token) <= 0) break; string bestPre = ""; string bestSuf = ""; int bestLen = -1; int tightest = 0; for(int i = 0; i < catalogue; i++) { const string name = SymbolName(i, false); string pre = ""; string suf = ""; if(!UQSymbolExtension(name, token, pre, suf)) continue; const int len = StringLen(pre) + StringLen(suf); if(bestLen < 0 || len < bestLen) { bestLen = len; bestPre = pre; bestSuf = suf; tightest = 1; continue; } if(len == bestLen) tightest++; } //--- a probe with no candidate, or with several equally tight //--- candidates, cannot vote for a convention if(tightest != 1) continue; const string key = bestPre + "|" + bestSuf; int slot = -1; for(int k = 0; k < distinct; k++) { if(keys[k] == key) { slot = k; break; } } if(slot < 0) { ArrayResize(keys, distinct + 1); ArrayResize(counts, distinct + 1); keys[distinct] = key; counts[distinct] = 0; slot = distinct; distinct++; } counts[slot]++; totalVote++; } if(distinct <= 0 || totalVote <= 0) return(false); int bestVotes = 0; int best = -1; for(int k = 0; k < distinct; k++) { if(counts[k] > bestVotes) { bestVotes = counts[k]; best = k; } } if(best < 0 || bestVotes < UQ_SYMBOL_PROBE_MIN_VOTES) return(false); if(bestVotes * 2 <= totalVote) return(false); // a weak majority is not a verified convention const string key = keys[best]; const int sep = StringFind(key, "|"); if(sep < 0) return(false); tagPrefix = StringSubstr(key, 0, sep); tagSuffix = StringSubstr(key, sep + 1); return(true); } //+------------------------------------------------------------------+ //| Fail-closed deterministic resolution of one requested symbol. | //| | //| 1. unique exact catalogue name, in either letter case | //| 2. unique eligible broker prefix/suffix variant | //| 3. the verified broker convention, when it leaves exactly one | //| 4. otherwise EMPTY / NOT_FOUND / AMBIGUOUS / INELIGIBLE | //+------------------------------------------------------------------+ ENUM_UQ_RESOLVE UQResolveSymbol(const string requested, const string tagPrefix, const string tagSuffix, string &resolved, string &detail) { resolved = ""; detail = ""; string token = requested; StringTrimLeft(token); StringTrimRight(token); if(StringLen(token) <= 0) { detail = "the requested symbol is empty"; return(UQ_RESOLVE_EMPTY); } string tokenUpper = token; StringToUpper(tokenUpper); //--- 1) exact catalogue name string exact = ""; int exactCount = 0; const int catalogue = SymbolsTotal(false); for(int i = 0; i < catalogue; i++) { const string name = SymbolName(i, false); if(StringLen(name) <= 0) continue; string upper = name; StringToUpper(upper); if(upper != tokenUpper) continue; exact = name; exactCount++; } if(exactCount > 1) { detail = "more than one catalogue symbol matches the request exactly"; return(UQ_RESOLVE_AMBIGUOUS); } if(exactCount == 1) { SymbolSelect(exact, true); string why = ""; if(!UQSymbolEligible(exact, why)) { detail = "'" + exact + "' cannot be traded by this strategy: " + why; return(UQ_RESOLVE_INELIGIBLE); } resolved = exact; return(UQ_RESOLVE_OK); } //--- 2) broker prefix/suffix variants string candidates[UQ_SYMBOL_CANDIDATE_MAX]; int candidateCount = 0; for(int i = 0; i < catalogue; i++) { const string name = SymbolName(i, false); if(StringLen(name) <= 0) continue; string pre = ""; string suf = ""; if(!UQSymbolExtension(name, token, pre, suf)) continue; if(StringLen(pre) + StringLen(suf) == 0) continue; // identical name, handled above if(candidateCount >= UQ_SYMBOL_CANDIDATE_MAX) { detail = "too many broker variants carry the requested token; specify the exact broker symbol name"; return(UQ_RESOLVE_AMBIGUOUS); } candidates[candidateCount] = name; candidateCount++; } if(candidateCount <= 0) { detail = "no broker symbol carries the requested token"; return(UQ_RESOLVE_NOT_FOUND); } //--- only an eligible instrument may be selected string eligible[UQ_SYMBOL_CANDIDATE_MAX]; int eligibleCount = 0; string eligibleList = ""; string firstReason = ""; for(int i = 0; i < candidateCount; i++) { SymbolSelect(candidates[i], true); string why = ""; if(UQSymbolEligible(candidates[i], why)) { eligible[eligibleCount] = candidates[i]; eligibleCount++; if(StringLen(eligibleList) > 0) eligibleList += ", "; eligibleList += candidates[i]; continue; } if(StringLen(firstReason) == 0) firstReason = "'" + candidates[i] + "' cannot be traded by this strategy: " + why; } if(eligibleCount == 1) { resolved = eligible[0]; return(UQ_RESOLVE_OK); } if(eligibleCount <= 0) { detail = (StringLen(firstReason) > 0 ? firstReason : "no eligible FX instrument carries the requested token"); return(UQ_RESOLVE_INELIGIBLE); } //--- 3) the verified broker convention breaks the tie if(StringLen(tagPrefix) + StringLen(tagSuffix) > 0) { string tagged[UQ_SYMBOL_CANDIDATE_MAX]; int taggedCount = 0; string taggedList = ""; for(int i = 0; i < eligibleCount; i++) { string pre = ""; string suf = ""; if(!UQSymbolExtension(eligible[i], token, pre, suf)) continue; if(pre != tagPrefix || suf != tagSuffix) continue; tagged[taggedCount] = eligible[i]; taggedCount++; if(StringLen(taggedList) > 0) taggedList += ", "; taggedList += eligible[i]; } if(taggedCount == 1) { resolved = tagged[0]; return(UQ_RESOLVE_OK); } if(taggedCount > 1) { detail = "the verified broker convention still matches several instruments: " + taggedList; return(UQ_RESOLVE_AMBIGUOUS); } } detail = "ambiguous request; eligible candidates: " + eligibleList; return(UQ_RESOLVE_AMBIGUOUS); } //+------------------------------------------------------------------+ //| Symbol registry and signal engine | //+------------------------------------------------------------------+ class CUQSymbols { private: UQSymbol m_sym[]; int m_count; int m_skipped; bool m_requireAll; ENUM_TIMEFRAMES m_tf; int m_channel; int m_emaPeriod; int m_atrPeriod; string m_tagPrefix; string m_tagSuffix; string m_lastError; public: CUQSymbols(void); ~CUQSymbols(void); bool Init(const ENUM_UQ_SYMBOL_MODE mode, const string list, const ENUM_TIMEFRAMES tf, const int channel, const int emaPeriod, const int atrPeriod, const bool requireAll); void Deinit(void); int Count(void) const { return(m_count); } int Skipped(void) const { return(m_skipped); } string LastError(void) const { return(m_lastError); } string TagText(void) const; string Name(const int idx) const; string Requested(const int idx) const; int FindByName(const string name) const; datetime LastEntryBar(const int idx) const; void SetLastEntryBar(const int idx, const datetime t); bool EnsureHandles(const int idx); bool IsNewBar(const int idx, datetime &barTime); bool EvaluateSignal(const int idx, int &direction, double &atrOut, string &reason); double AtrValue(const int idx); void Note(const int idx, const string text, const bool throttle); }; //+------------------------------------------------------------------+ CUQSymbols::CUQSymbols(void) { m_count = 0; m_skipped = 0; m_requireAll = true; m_tf = PERIOD_H1; m_channel = 20; m_emaPeriod = 100; m_atrPeriod = 14; m_tagPrefix = ""; m_tagSuffix = ""; m_lastError = ""; ArrayResize(m_sym, 0); } //+------------------------------------------------------------------+ CUQSymbols::~CUQSymbols(void) { Deinit(); } //+------------------------------------------------------------------+ //| Verified broker prefix/suffix convention, for the status display | //+------------------------------------------------------------------+ string CUQSymbols::TagText(void) const { if(StringLen(m_tagPrefix) + StringLen(m_tagSuffix) == 0) return("unverified"); return("'" + m_tagPrefix + "" + m_tagSuffix + "'"); } //+------------------------------------------------------------------+ //| Build the managed symbol set | //| | //| Every configured token is resolved deterministically. A token | //| that cannot be resolved to exactly one eligible instrument is a | //| configuration error, never a silently dropped symbol, unless the | //| operator explicitly allowed a partial set. | //+------------------------------------------------------------------+ bool CUQSymbols::Init(const ENUM_UQ_SYMBOL_MODE mode, const string list, const ENUM_TIMEFRAMES tf, const int channel, const int emaPeriod, const int atrPeriod, const bool requireAll) { m_tf = tf; m_channel = channel; m_emaPeriod = emaPeriod; m_atrPeriod = atrPeriod; m_requireAll = requireAll; m_lastError = ""; m_count = 0; m_skipped = 0; m_tagPrefix = ""; m_tagSuffix = ""; ArrayResize(m_sym, 0); //--- the broker convention is inferred once, from a fixed probe set if(UQDetectBrokerTag(m_tagPrefix, m_tagSuffix)) PrintFormat("[UQ] Broker symbol convention verified: prefix='%s' suffix='%s'.", m_tagPrefix, m_tagSuffix); else PrintFormat("[UQ] Broker symbol convention could not be verified; only exact and unambiguous names are accepted."); string tokens[]; int nTok = 0; if(mode == UQ_SYM_CURRENT_CHART) { ArrayResize(tokens, 1); tokens[0] = _Symbol; nTok = 1; } else { string src = list; StringReplace(src, ";", ","); nTok = StringSplit(src, ',', tokens); } if(nTok <= 0) { m_lastError = "no symbols are configured"; return(false); } for(int i = 0; i < nTok; i++) { string t = tokens[i]; StringTrimLeft(t); StringTrimRight(t); if(StringLen(t) == 0) continue; bool duplicate = false; for(int k = 0; k < m_count; k++) { if(StringCompare(m_sym[k].requested, t, false) == 0) { duplicate = true; break; } } if(duplicate) { PrintFormat("[UQ] WARNING: symbol '%s' is configured twice and is ignored the second time.", t); continue; } string resolved = ""; string detail = ""; const ENUM_UQ_RESOLVE outcome = UQResolveSymbol(t, m_tagPrefix, m_tagSuffix, resolved, detail); if(outcome != UQ_RESOLVE_OK) { m_skipped++; PrintFormat("[UQ] SYMBOL REJECTED '%s': %s (%s).", t, UQResolveText(outcome), detail); if(m_requireAll) { m_lastError = StringFormat("symbol '%s' could not be resolved: %s (%s)", t, UQResolveText(outcome), detail); return(false); } continue; } //--- two tokens may not resolve to the same broker instrument if(FindByName(resolved) >= 0) { PrintFormat("[UQ] WARNING: '%s' resolves to '%s', which is already managed by an earlier token; it is ignored.", t, resolved); continue; } ArrayResize(m_sym, m_count + 1); m_sym[m_count].requested = t; m_sym[m_count].name = resolved; m_sym[m_count].resolved = true; m_sym[m_count].hEma = INVALID_HANDLE; m_sym[m_count].hAtr = INVALID_HANDLE; m_sym[m_count].lastBar = 0; m_sym[m_count].lastEntryBar = 0; m_sym[m_count].lastLogTime = 0; m_sym[m_count].lastNote = ""; m_count++; if(resolved == t) PrintFormat("[UQ] Symbol '%s' resolved exactly to '%s'.", t, resolved); else PrintFormat("[UQ] Symbol '%s' resolved to broker symbol '%s'.", t, resolved); } if(m_count <= 0) { if(StringLen(m_lastError) <= 0) m_lastError = "no usable symbol could be resolved"; return(false); } //--- indicator handles are created once, during initialisation for(int i = 0; i < m_count; i++) { if(!EnsureHandles(i)) { m_lastError = StringFormat("indicator handles could not be created for '%s'", m_sym[i].name); return(false); } } return(true); } //+------------------------------------------------------------------+ //| Release indicator handles | //+------------------------------------------------------------------+ void CUQSymbols::Deinit(void) { for(int i = 0; i < m_count; i++) { if(m_sym[i].hEma != INVALID_HANDLE) { IndicatorRelease(m_sym[i].hEma); m_sym[i].hEma = INVALID_HANDLE; } if(m_sym[i].hAtr != INVALID_HANDLE) { IndicatorRelease(m_sym[i].hAtr); m_sym[i].hAtr = INVALID_HANDLE; } } } //+------------------------------------------------------------------+ string CUQSymbols::Name(const int idx) const { if(idx < 0 || idx >= m_count) return(""); return(m_sym[idx].name); } //+------------------------------------------------------------------+ string CUQSymbols::Requested(const int idx) const { if(idx < 0 || idx >= m_count) return(""); return(m_sym[idx].requested); } //+------------------------------------------------------------------+ int CUQSymbols::FindByName(const string name) const { for(int i = 0; i < m_count; i++) { if(m_sym[i].name == name) return(i); } return(-1); } //+------------------------------------------------------------------+ datetime CUQSymbols::LastEntryBar(const int idx) const { if(idx < 0 || idx >= m_count) return(0); return(m_sym[idx].lastEntryBar); } //+------------------------------------------------------------------+ void CUQSymbols::SetLastEntryBar(const int idx, const datetime t) { if(idx < 0 || idx >= m_count) return; m_sym[idx].lastEntryBar = t; } //+------------------------------------------------------------------+ //| Create indicator handles on first use and validate readiness | //+------------------------------------------------------------------+ bool CUQSymbols::EnsureHandles(const int idx) { if(idx < 0 || idx >= m_count) return(false); const string s = m_sym[idx].name; if(m_sym[idx].hEma == INVALID_HANDLE) { m_sym[idx].hEma = iMA(s, m_tf, m_emaPeriod, 0, MODE_EMA, PRICE_CLOSE); if(m_sym[idx].hEma == INVALID_HANDLE) { Note(idx, StringFormat("EMA handle creation failed (%d)", GetLastError()), true); return(false); } } if(m_sym[idx].hAtr == INVALID_HANDLE) { m_sym[idx].hAtr = iATR(s, m_tf, m_atrPeriod); if(m_sym[idx].hAtr == INVALID_HANDLE) { Note(idx, StringFormat("ATR handle creation failed (%d)", GetLastError()), true); return(false); } } //--- readiness is decided by the actual buffer copies, not by a cached //--- BarsCalculated() value, which can stay unset for handles created //--- inside the strategy tester. const int emaBars = BarsCalculated(m_sym[idx].hEma); const int atrBars = BarsCalculated(m_sym[idx].hAtr); if(emaBars == -1 && atrBars == -1) Note(idx, StringFormat("indicator buffers still initialising (ema bars=%d, atr bars=%d)", emaBars, atrBars), true); return(true); } //+------------------------------------------------------------------+ //| New-bar detection. The first observation only seeds the state so | //| that attaching mid-bar cannot trigger a late historical entry. | //+------------------------------------------------------------------+ bool CUQSymbols::IsNewBar(const int idx, datetime &barTime) { barTime = 0; if(idx < 0 || idx >= m_count) return(false); datetime t[]; if(CopyTime(m_sym[idx].name, m_tf, 0, 1, t) < 1) return(false); barTime = t[0]; if(barTime <= 0) return(false); if(barTime == m_sym[idx].lastBar) return(false); const bool firstObservation = (m_sym[idx].lastBar == 0); m_sym[idx].lastBar = barTime; if(firstObservation) return(false); return(true); } //+------------------------------------------------------------------+ //| ATR of the last completed signal bar | //+------------------------------------------------------------------+ double CUQSymbols::AtrValue(const int idx) { if(idx < 0 || idx >= m_count) return(0.0); if(!EnsureHandles(idx)) return(0.0); double atr[]; ArraySetAsSeries(atr, true); if(CopyBuffer(m_sym[idx].hAtr, 0, 0, 3, atr) < 3) return(0.0); if(!MathIsValidNumber(atr[1]) || atr[1] <= 0.0) return(0.0); return(atr[1]); } //+------------------------------------------------------------------+ //| Evaluate the breakout signal on completed candles only. | //| direction: +1 long, -1 short, 0 no signal | //+------------------------------------------------------------------+ bool CUQSymbols::EvaluateSignal(const int idx, int &direction, double &atrOut, string &reason) { direction = 0; atrOut = 0.0; reason = ""; if(idx < 0 || idx >= m_count) { reason = "invalid symbol index"; return(false); } if(!m_sym[idx].resolved) { reason = "symbol is not resolved"; return(false); } if(!EnsureHandles(idx)) { reason = "indicators are not ready"; return(false); } const string s = m_sym[idx].name; const int sdig = (int)SymbolInfoInteger(s, SYMBOL_DIGITS); const int need = m_channel + 5; double high[], low[], close[]; datetime times[]; ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); ArraySetAsSeries(times, true); const int nHigh = CopyHigh(s, m_tf, 0, need, high); const int nLow = CopyLow(s, m_tf, 0, need, low); const int nClose = CopyClose(s, m_tf, 0, need, close); const int nTime = CopyTime(s, m_tf, 0, need, times); if(nHigh < need || nLow < need || nClose < need || nTime < need) { reason = StringFormat("history not ready (high %d, low %d, close %d, time %d, need %d)", nHigh, nLow, nClose, nTime, need); return(false); } //--- channel of the completed bars preceding the last completed bar double channelHigh = -DBL_MAX; double channelLow = DBL_MAX; for(int i = 2; i <= m_channel + 1; i++) { if(!MathIsValidNumber(high[i]) || !MathIsValidNumber(low[i])) { reason = "invalid channel price data"; return(false); } if(high[i] > channelHigh) channelHigh = high[i]; if(low[i] < channelLow) channelLow = low[i]; } double ema[], atr[]; ArraySetAsSeries(ema, true); ArraySetAsSeries(atr, true); if(CopyBuffer(m_sym[idx].hEma, 0, 0, 3, ema) < 3) { reason = "EMA buffer is not available"; return(false); } if(CopyBuffer(m_sym[idx].hAtr, 0, 0, 3, atr) < 3) { reason = "ATR buffer is not available"; return(false); } const double lastClose = close[1]; const double emaValue = ema[1]; const double atrValue = atr[1]; if(!MathIsValidNumber(lastClose) || lastClose <= 0.0) { reason = "invalid completed close price"; return(false); } if(!MathIsValidNumber(emaValue) || emaValue <= 0.0) { reason = "invalid EMA value"; return(false); } if(!MathIsValidNumber(atrValue) || atrValue <= 0.0) { reason = "invalid ATR value"; return(false); } atrOut = atrValue; if(lastClose > channelHigh && lastClose > emaValue) { direction = 1; reason = StringFormat("close %s > channelHigh %s and EMA %s", DoubleToString(lastClose, sdig), DoubleToString(channelHigh, sdig), DoubleToString(emaValue, sdig)); return(true); } if(lastClose < channelLow && lastClose < emaValue) { direction = -1; reason = StringFormat("close %s < channelLow %s and EMA %s", DoubleToString(lastClose, sdig), DoubleToString(channelLow, sdig), DoubleToString(emaValue, sdig)); return(true); } return(true); } //+------------------------------------------------------------------+ //| Throttled per-symbol diagnostic note | //+------------------------------------------------------------------+ void CUQSymbols::Note(const int idx, const string text, const bool throttle) { if(idx < 0 || idx >= m_count) return; const datetime now = TimeCurrent(); if(throttle && m_sym[idx].lastNote == text && (now - m_sym[idx].lastLogTime) < 300) return; m_sym[idx].lastNote = text; m_sym[idx].lastLogTime = now; PrintFormat("[UQ] %s: %s", m_sym[idx].name, text); } #endif // UQ_SYMBOLS_MQH //+------------------------------------------------------------------+