2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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. |
|
2026-10-30 09:19:45 +07:00 | | | //| |
|
| | | //| 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. |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | #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;
|
| | | };
|
| | |
|
2026-10-30 09:19:45 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| Symbol registry and signal engine |
|
| | | //+------------------------------------------------------------------+
|
| | | class CUQSymbols
|
| | | {
|
| | | private:
|
| | | UQSymbol m_sym[];
|
| | | int m_count;
|
2026-10-30 09:19:45 +07:00 | | | int m_skipped;
|
| | | bool m_requireAll;
|
2026-10-30 07:19:59 +07:00 | | | ENUM_TIMEFRAMES m_tf;
|
| | | int m_channel;
|
| | | int m_emaPeriod;
|
| | | int m_atrPeriod;
|
2026-10-30 09:19:45 +07:00 | | | string m_tagPrefix;
|
| | | string m_tagSuffix;
|
2026-10-30 07:19:59 +07:00 | | | 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,
|
2026-10-30 09:19:45 +07:00 | | | const int emaPeriod, const int atrPeriod,
|
| | | const bool requireAll);
|
2026-10-30 07:19:59 +07:00 | | | void Deinit(void);
|
| | |
|
| | | int Count(void) const { return(m_count); }
|
2026-10-30 09:19:45 +07:00 | | | int Skipped(void) const { return(m_skipped); }
|
2026-10-30 07:19:59 +07:00 | | | string LastError(void) const { return(m_lastError); }
|
2026-10-30 09:19:45 +07:00 | | | string TagText(void) const;
|
2026-10-30 07:19:59 +07:00 | | |
|
| | | 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;
|
2026-10-30 09:19:45 +07:00 | | | m_skipped = 0;
|
| | | m_requireAll = true;
|
2026-10-30 07:19:59 +07:00 | | | m_tf = PERIOD_H1;
|
| | | m_channel = 20;
|
| | | m_emaPeriod = 100;
|
| | | m_atrPeriod = 14;
|
2026-10-30 09:19:45 +07:00 | | | m_tagPrefix = "";
|
| | | m_tagSuffix = "";
|
2026-10-30 07:19:59 +07:00 | | | m_lastError = "";
|
| | | ArrayResize(m_sym, 0);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | CUQSymbols::~CUQSymbols(void)
|
| | | {
|
| | | Deinit();
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-10-30 09:19:45 +07:00 | | | //| Verified broker prefix/suffix convention, for the status display |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
2026-10-30 09:19:45 +07:00 | | | string CUQSymbols::TagText(void) const
|
2026-10-30 07:19:59 +07:00 | | | {
|
2026-10-30 09:19:45 +07:00 | | | if(StringLen(m_tagPrefix) + StringLen(m_tagSuffix) == 0)
|
| | | return("unverified");
|
| | | return("'" + m_tagPrefix + "<pair>" + m_tagSuffix + "'");
|
2026-10-30 07:19:59 +07:00 | | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Build the managed symbol set |
|
2026-10-30 09:19:45 +07:00 | | | //| |
|
| | | //| 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. |
|
2026-10-30 07:19:59 +07:00 | | | //+------------------------------------------------------------------+
|
| | | bool CUQSymbols::Init(const ENUM_UQ_SYMBOL_MODE mode, const string list,
|
| | | const ENUM_TIMEFRAMES tf, const int channel,
|
2026-10-30 09:19:45 +07:00 | | | const int emaPeriod, const int atrPeriod,
|
| | | const bool requireAll)
|
2026-10-30 07:19:59 +07:00 | | | {
|
2026-10-30 09:19:45 +07:00 | | | 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 = "";
|
2026-10-30 07:19:59 +07:00 | | | ArrayResize(m_sym, 0);
|
| | |
|
2026-10-30 09:19:45 +07:00 | | | //--- 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.");
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | 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)
|
2026-10-30 09:19:45 +07:00 | | | {
|
| | | PrintFormat("[UQ] WARNING: symbol '%s' is configured twice and is ignored the second time.", t);
|
2026-10-30 07:19:59 +07:00 | | | continue;
|
2026-10-30 09:19:45 +07:00 | | | }
|
2026-10-30 07:19:59 +07:00 | | |
|
| | | string resolved = "";
|
2026-10-30 09:19:45 +07:00 | | | 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)
|
2026-10-30 07:19:59 +07:00 | | | {
|
2026-10-30 09:19:45 +07:00 | | | PrintFormat("[UQ] WARNING: '%s' resolves to '%s', which is already managed by an earlier token; it is ignored.",
|
| | | t, resolved);
|
2026-10-30 07:19:59 +07:00 | | | 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++;
|
| | |
|
2026-10-30 09:19:45 +07:00 | | | 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);
|
2026-10-30 07:19:59 +07:00 | | | }
|
| | |
|
| | | if(m_count <= 0)
|
| | | {
|
2026-10-30 09:19:45 +07:00 | | | if(StringLen(m_lastError) <= 0)
|
| | | m_lastError = "no usable symbol could be resolved";
|
2026-10-30 07:19:59 +07:00 | | | return(false);
|
| | | }
|
2026-10-30 07:33:16 +07:00 | | |
|
| | | //--- 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);
|
| | | }
|
| | | }
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | 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);
|
| | | }
|
| | | }
|
| | |
|
2026-10-30 07:33:16 +07:00 | | | //--- 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);
|
| | |
|
2026-10-30 07:19:59 +07:00 | | | 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
|
| | | //+------------------------------------------------------------------+
|