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