//+------------------------------------------------------------------+ //| CrossAsset.mqh | //| AnimateDread | //| | //| Information that is NOT a transform of the traded symbol's own | //| OHLCV series: what every OTHER instrument was doing at the same | //| moment. | //+------------------------------------------------------------------+ #ifndef WARRIOR_CROSSASSET_MQH #define WARRIOR_CROSSASSET_MQH #define CROSSASSET_MAX_PAIRS 64 #define CROSSASSET_MAX_CCY 16 #define CROSSASSET_FEATURES 6 //--- momentum horizons, in bars of the traded timeframe #define CROSSASSET_FAST_BARS 1 #define CROSSASSET_SLOW_BARS 20 //--- a reference pair must cover at least this fraction of the traded symbol's bars to be used #define CROSSASSET_MIN_COVERAGE 0.80 //+------------------------------------------------------------------+ //| Cross-asset panel, aligned to one symbol/timeframe bar grid. | //+------------------------------------------------------------------+ class CCrossAssetPanel { private: string m_symbol; ENUM_TIMEFRAMES m_period; int m_bars; // length of every array below bool m_ready; string m_baseCcy; string m_quoteCcy; //--- cumulative log strength per currency, [currency][bar], bar 0 = NEWEST (series order, //--- matching every other buffer in this codebase) string m_ccy[CROSSASSET_MAX_CCY]; int m_ccyCount; double m_strength[]; // flattened [ccyCount * m_bars] int m_baseIdx; int m_quoteIdx; //--- index mode only (m_baseIdx == m_quoteIdx): the risk-proxy currency's slot, -1 when none int m_riskIdx; //--- Train->serve parity: the pair set a model was TRAINED on, adopted from the .cfg. A pinned //--- pair that is temporarily unavailable is skipped for that build (degraded, same as before), //--- never substituted. string m_pinnedPairs[]; //--- the pairs the LAST successful Build actually used - what a fresh model pins to its .cfg string m_usedPairsCsv; //--- cross-sectional dispersion of currency moves: a risk-on/risk-off proxy that belongs to no //--- single currency (wide dispersion = a directional FX day, narrow = drift/chop) double m_dispersion[]; bool DiscoverPairs(string &pairs[], string &pbase[], string &pquote[]); bool SeriesReady(string sym, ENUM_TIMEFRAMES tf, int need); int CurrencySlot(string ccy); int FindCurrency(string ccy) const; int SelectRiskProxy(void) const; double Strength(int ccyIdx, int bar) const { return(m_strength[ccyIdx * m_bars + bar]); } public: CCrossAssetPanel(void); ~CCrossAssetPanel(void); bool Build(string symbol, ENUM_TIMEFRAMES period, int bars); bool IsReady(void) const { return(m_ready); } int Bars(void) const { return(m_bars); } //--- CROSSASSET_FEATURES values for bar idx, all scale-free. Returns false only if the panel //--- was never built; a bar too close to the oldest edge yields a neutral 0-fill instead, the //--- same "degraded but usable" convention BufferTempDataCompute() uses for its swing block. bool Features(int idx, double &out[]); //--- pin/report the reference-pair set (comma-separated), see m_pinnedPairs void SetPinnedPairs(string csv); string UsedPairsCsv(void) const { return(m_usedPairsCsv); } bool HasPinnedPairs(void) const { return(ArraySize(m_pinnedPairs) > 0); } //--- Kick the terminal's ASYNCHRONOUS cross-symbol sync as early as possible (OnInit), so the //--- history download runs while the EA is still setting up instead of starting only when the //--- first Build() call trips over an unselected symbol. Non-blocking, exactly like SeriesReady. void Warm(ENUM_TIMEFRAMES period); //--- BLOCKING sibling of Warm(), called once from OnInit BEFORE any model exists (same fix as //--- CAltDataFetch's OnInit warm - see its call site's comment): a fresh model's FIRST //--- successful Build() pins whatever pair set that call actually used, for life (see //--- m_pinnedPairs). bool WarmBlocking(ENUM_TIMEFRAMES period, int timeoutMs); }; //+------------------------------------------------------------------+ CCrossAssetPanel::CCrossAssetPanel(void) : m_symbol(""), m_period(PERIOD_CURRENT), m_bars(0), m_ready(false), m_baseCcy(""), m_quoteCcy(""), m_ccyCount(0), m_baseIdx(-1), m_quoteIdx(-1), m_riskIdx(-1) { } //+------------------------------------------------------------------+ CCrossAssetPanel::~CCrossAssetPanel(void) { } //+------------------------------------------------------------------+ //| Slot for a currency code, appending if new. -1 when full. | //+------------------------------------------------------------------+ int CCrossAssetPanel::CurrencySlot(string ccy) { for(int i = 0; i < m_ccyCount; i++) if(m_ccy[i] == ccy) return(i); if(m_ccyCount >= CROSSASSET_MAX_CCY) return(-1); m_ccy[m_ccyCount] = ccy; m_ccyCount++; return(m_ccyCount - 1); } //+------------------------------------------------------------------+ //| Adopt a pinned pair set ("EURUSD,USDJPY,..."), or clear with "". | //+------------------------------------------------------------------+ void CCrossAssetPanel::SetPinnedPairs(string csv) { ArrayResize(m_pinnedPairs, 0); string parts[]; int n = StringSplit(csv, ',', parts); for(int i = 0; i < n; i++) { StringTrimLeft(parts[i]); StringTrimRight(parts[i]); if(parts[i] == "") continue; int k = ArraySize(m_pinnedPairs); ArrayResize(m_pinnedPairs, k + 1); m_pinnedPairs[k] = parts[i]; } } //+------------------------------------------------------------------+ //| Start the async sync for every reference candidate NOW. Each | //| SymbolSelect/SeriesInfo touch queues a history build on the | //| terminal's own threads; by the time the first real Build() runs | //| the download has had its head start instead of starting there. | //+------------------------------------------------------------------+ void CCrossAssetPanel::Warm(ENUM_TIMEFRAMES period) { string pairs[], pbase[], pquote[]; if(HasPinnedPairs()) { //--- a trained model warms exactly the set it will use int n = ArraySize(m_pinnedPairs); ArrayResize(pairs, n); for(int i = 0; i < n; i++) pairs[i] = m_pinnedPairs[i]; } else if(!DiscoverPairs(pairs, pbase, pquote)) return; for(int i = 0; i < ArraySize(pairs); i++) SeriesReady(pairs[i], period, 1); // selects + touches; result deliberately ignored } //+------------------------------------------------------------------+ bool CCrossAssetPanel::WarmBlocking(ENUM_TIMEFRAMES period, int timeoutMs) { string pairs[], pbase[], pquote[]; if(HasPinnedPairs()) { int n = ArraySize(m_pinnedPairs); ArrayResize(pairs, n); for(int i = 0; i < n; i++) pairs[i] = m_pinnedPairs[i]; } //--- NOTHING TO WAIT FOR is success, not failure. Only an expired wait with pairs still unsynced //--- returns false. else if(!DiscoverPairs(pairs, pbase, pquote)) return true; int n = ArraySize(pairs); if(n == 0) return true; uint start = GetTickCount(); bool allReady = false; while(!IsStopped()) { int ready = 0; for(int i = 0; i < n; i++) if(SeriesReady(pairs[i], period, 1)) ready++; if(ready >= n) { allReady = true; break; } if((int)(GetTickCount() - start) >= timeoutMs) break; Sleep(200); } return allReady; } //+------------------------------------------------------------------+ //| Lookup-only twin of CurrencySlot: never appends. -1 when absent. | //+------------------------------------------------------------------+ int CCrossAssetPanel::FindCurrency(string ccy) const { for(int i = 0; i < m_ccyCount; i++) if(m_ccy[i] == ccy) return(i); return(-1); } //+------------------------------------------------------------------+ //| Index mode's risk-proxy currency. | //+------------------------------------------------------------------+ int CCrossAssetPanel::SelectRiskProxy(void) const { string prefs[] = {"JPY", "CHF", "EUR", "GBP", "AUD", "NZD", "CAD"}; for(int i = 0; i < ArraySize(prefs); i++) { if(prefs[i] == m_baseCcy) continue; int slot = FindCurrency(prefs[i]); if(slot >= 0) return(slot); } //--- none of the preferred codes present: first registered currency that is not the denomination for(int i = 0; i < m_ccyCount; i++) if(m_ccy[i] != m_baseCcy) return(i); return(-1); } //+------------------------------------------------------------------+ //| Every Market Watch symbol that is an FX pair sharing a currency | //| with the traded symbol. Market Watch rather than the full broker | //| list on purpose: the full list can run to thousands of symbols, | //| most of them untraded CFDs whose history the terminal has never | //| downloaded, and forcing a sync on each would stall init. | //+------------------------------------------------------------------+ bool CCrossAssetPanel::DiscoverPairs(string &pairs[], string &pbase[], string &pquote[]) { ArrayResize(pairs, 0); ArrayResize(pbase, 0); ArrayResize(pquote, 0); int total = SymbolsTotal(true); for(int i = 0; i < total && ArraySize(pairs) < CROSSASSET_MAX_PAIRS; i++) { string s = SymbolName(i, true); if(s == "") continue; string b = SymbolInfoString(s, SYMBOL_CURRENCY_BASE); string q = SymbolInfoString(s, SYMBOL_CURRENCY_PROFIT); if(b == "" || q == "" || b == q) continue; //--- Only true FX crosses carry a clean "which currency moved" reading. A CFD on an index or //--- a metal reports a currency pair (e.g. if(StringLen(b) != 3 || StringLen(q) != 3) continue; if(SymbolInfoInteger(s, SYMBOL_TRADE_CALC_MODE) != SYMBOL_CALC_MODE_FOREX) continue; int n = ArraySize(pairs); ArrayResize(pairs, n + 1); ArrayResize(pbase, n + 1); ArrayResize(pquote, n + 1); pairs[n] = s; pbase[n] = b; pquote[n] = q; } return(ArraySize(pairs) > 0); } //+------------------------------------------------------------------+ //| Is sym/tf genuinely ready to be read to `need` bars? | //+------------------------------------------------------------------+ bool CCrossAssetPanel::SeriesReady(string sym, ENUM_TIMEFRAMES tf, int need) { if(!(bool)SymbolInfoInteger(sym, SYMBOL_SELECT)) { //--- ask the terminal to start tracking it; it will not be ready THIS call SymbolSelect(sym, true); return(false); } if(!SymbolIsSynchronized(sym)) return(false); long synced = 0; if(!SeriesInfoInteger(sym, tf, SERIES_SYNCHRONIZED, synced) || synced == 0) return(false); long haveBars = 0; if(!SeriesInfoInteger(sym, tf, SERIES_BARS_COUNT, haveBars)) return(false); return(haveBars >= (long)(CROSSASSET_MIN_COVERAGE * need)); } //+------------------------------------------------------------------+ //| Build the panel for `bars` bars ending at the newest closed bar. | //+------------------------------------------------------------------+ bool CCrossAssetPanel::Build(string symbol, ENUM_TIMEFRAMES period, int bars) { m_ready = false; m_symbol = symbol; m_period = period; m_bars = bars; m_ccyCount = 0; m_baseIdx = m_quoteIdx = m_riskIdx = -1; if(bars <= CROSSASSET_SLOW_BARS + 2) return(false); m_baseCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE); m_quoteCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT); if(m_baseCcy == "" || m_quoteCcy == "") return(false); //--- the traded symbol's own bar grid: every reference series is resampled onto these timestamps datetime times[]; //--- Series flag BEFORE the copy, not after: this is what makes times[0] the NEWEST bar and so //--- match the index convention every caller here uses (BufferTempDataCompute's idx). Setting it //--- afterwards happens to work, but the whole panel is index arithmetic against this grid and a //--- silently reversed axis would not fail - it would just train on mirrored history. ArraySetAsSeries(times, true); if(CopyTime(symbol, period, 0, bars, times) != bars) { Print(__FUNCTION__ + ": could not read " + IntegerToString(bars) + " bar times for " + symbol + " - cross-asset features unavailable this run."); return(false); } string pairs[], pbase[], pquote[]; if(HasPinnedPairs()) { //--- A trained model builds from its PINNED set, never from discovery: Market Watch is //--- mutable terminal state, and a model whose features were trained against one pair set //--- must not have their meaning drift because a symbol was added or removed. for(int i = 0; i < ArraySize(m_pinnedPairs); i++) { string b = SymbolInfoString(m_pinnedPairs[i], SYMBOL_CURRENCY_BASE); string q = SymbolInfoString(m_pinnedPairs[i], SYMBOL_CURRENCY_PROFIT); if(b == "" || q == "" || b == q) continue; int n = ArraySize(pairs); ArrayResize(pairs, n + 1); ArrayResize(pbase, n + 1); ArrayResize(pquote, n + 1); pairs[n] = m_pinnedPairs[i]; pbase[n] = b; pquote[n] = q; } if(ArraySize(pairs) == 0) { Print(__FUNCTION__ + ": none of the PINNED reference pairs resolve in this terminal - " "cross-asset features unavailable. The pin lives in the model's .cfg; this terminal " "does not know those symbols."); return(false); } } else if(!DiscoverPairs(pairs, pbase, pquote)) { Print(__FUNCTION__ + ": no FX pairs in Market Watch - cross-asset features unavailable."); return(false); } //--- accumulate per-currency log-return sums and the count of contributing pairs, per bar int maxCcy = CROSSASSET_MAX_CCY; double sum[]; int cnt[]; ArrayResize(sum, maxCcy * bars); ArrayResize(cnt, maxCcy * bars); ArrayInitialize(sum, 0.0); ArrayInitialize(cnt, 0); int used = 0; int skippedUnsynced = 0; string usedCsv = ""; for(int p = 0; p < ArraySize(pairs); p++) { if(!SeriesReady(pairs[p], period, bars)) { skippedUnsynced++; continue; } //--- Resample this pair onto the traded grid. CopyClose by TIME RANGE (not by index) is what //--- makes this safe across symbols that keep different session calendars: index 5 on GBPUSD //--- and index 5 on USDJPY are not the same instant if either had a gap. double pc[]; if(CopyClose(pairs[p], period, times[bars - 1], times[0], pc) <= 0) continue; datetime pt[]; if(CopyTime(pairs[p], period, times[bars - 1], times[0], pt) <= 0) continue; int pn = ArraySize(pc); if(pn < 2 || ArraySize(pt) != pn) continue; //--- pc/pt come back oldest-first from a range copy; walk the traded grid newest->oldest and //--- carry a cursor backwards through the reference series, taking the last reference bar at //--- or BEFORE each traded timestamp. Never a bar after it - that would be lookahead. double aligned[]; ArrayResize(aligned, bars); ArrayInitialize(aligned, 0.0); //--- Staleness cap. But an unbounded "at or before" would happily match a quote from three //--- days ago across a market holiday and call it this bar's price. int stale = (int)PeriodSeconds(period); int cur = pn - 1; int covered = 0; for(int b = 0; b < bars; b++) // b = 0 is the NEWEST traded bar { while(cur > 0 && pt[cur] > times[b]) cur--; if(pt[cur] <= times[b] && (int)(times[b] - pt[cur]) <= stale) { aligned[b] = pc[cur]; covered++; } } if(covered < (int)(CROSSASSET_MIN_COVERAGE * bars)) continue; // too gappy to be a trustworthy reference int bi = CurrencySlot(pbase[p]); int qi = CurrencySlot(pquote[p]); if(bi < 0 || qi < 0) continue; //--- one bar's log return, credited +ve to the base currency and -ve to the quote for(int b = 0; b < bars - 1; b++) { double a1 = aligned[b], a0 = aligned[b + 1]; if(a1 <= 0.0 || a0 <= 0.0) continue; double r = MathLog(a1 / a0); sum[bi * bars + b] += r; cnt[bi * bars + b] += 1; sum[qi * bars + b] -= r; cnt[qi * bars + b] += 1; } used++; usedCsv += (usedCsv == "" ? "" : ",") + pairs[p]; } if(used < 2 || m_ccyCount <= 0) { PrintFormat("%s: only %d usable reference pairs (%d skipped as unsynchronised/short) - " "cross-asset features unavailable this build, need >= 2. In the Strategy Tester the " "agent loads auxiliary symbols from the TERMINAL, not the server, so any reference " "pair whose history was never downloaded is permanently absent for that run.", __FUNCTION__, used, skippedUnsynced); return(false); } //--- per-bar average return per currency, then integrate newest<-oldest into a strength LEVEL so //--- multi-bar momentum is a plain difference of two levels ArrayResize(m_strength, m_ccyCount * bars); ArrayInitialize(m_strength, 0.0); ArrayResize(m_dispersion, bars); ArrayInitialize(m_dispersion, 0.0); for(int b = bars - 2; b >= 0; b--) { double mean = 0.0; int have = 0; double vals[CROSSASSET_MAX_CCY]; for(int cIdx = 0; cIdx < m_ccyCount; cIdx++) { int k = cIdx * bars + b; double r = (cnt[k] > 0) ? sum[k] / cnt[k] : 0.0; vals[cIdx] = r; m_strength[k] = m_strength[cIdx * bars + b + 1] + r; if(cnt[k] > 0) { mean += r; have++; } } //--- cross-sectional standard deviation of this bar's currency moves if(have > 1) { mean /= have; double v = 0.0; for(int cIdx = 0; cIdx < m_ccyCount; cIdx++) if(cnt[cIdx * bars + b] > 0) v += (vals[cIdx] - mean) * (vals[cIdx] - mean); m_dispersion[b] = MathSqrt(v / (have - 1)); } } m_baseIdx = CurrencySlot(m_baseCcy); m_quoteIdx = CurrencySlot(m_quoteCcy); if(m_baseIdx < 0 || m_quoteIdx < 0) { Print(__FUNCTION__ + ": traded symbol's currencies (" + m_baseCcy + "/" + m_quoteCcy + ") are not covered by any Market Watch pair - cross-asset features unavailable."); return(false); } m_usedPairsCsv = usedCsv; if(m_baseIdx == m_quoteIdx) { m_riskIdx = SelectRiskProxy(); m_ready = true; PrintFormat("%s: cross-asset panel built - %d reference pairs, %d currencies, %d bars " "(INDEX MODE: %s-denominated, risk proxy %s)", __FUNCTION__, used, m_ccyCount, bars, m_baseCcy, m_riskIdx >= 0 ? m_ccy[m_riskIdx] : "NONE"); return(true); } m_ready = true; PrintFormat("%s: cross-asset panel built - %d reference pairs, %d currencies, %d bars (%s/%s)", __FUNCTION__, used, m_ccyCount, bars, m_baseCcy, m_quoteCcy); return(true); } //+------------------------------------------------------------------+ //| Features for bar idx (series order: 0 = newest). | //+------------------------------------------------------------------+ bool CCrossAssetPanel::Features(int idx, double &out[]) { if(ArraySize(out) != CROSSASSET_FEATURES) ArrayResize(out, CROSSASSET_FEATURES); ArrayInitialize(out, 0.0); if(!m_ready) return(false); if(idx < 0 || idx + CROSSASSET_SLOW_BARS + 1 >= m_bars) return(true); // degraded-but-usable neutral fill near the edges //--- Strength moves are log returns summed across pairs, so they already live in a small, //--- symbol-independent range. double c1 = iClose(m_symbol, m_period, idx); double c0 = iClose(m_symbol, m_period, idx + CROSSASSET_SLOW_BARS); if(m_baseIdx != m_quoteIdx) { //--- FX MODE: the traded symbol IS a currency pair, so its two sides each have an index. double bFast = (Strength(m_baseIdx, idx) - Strength(m_baseIdx, idx + CROSSASSET_FAST_BARS)) * 100.0; double qFast = (Strength(m_quoteIdx, idx) - Strength(m_quoteIdx, idx + CROSSASSET_FAST_BARS)) * 100.0; double bSlow = (Strength(m_baseIdx, idx) - Strength(m_baseIdx, idx + CROSSASSET_SLOW_BARS)) * 100.0; double qSlow = (Strength(m_quoteIdx, idx) - Strength(m_quoteIdx, idx + CROSSASSET_SLOW_BARS)) * 100.0; out[0] = MathMax(-5.0, MathMin(5.0, bFast)); out[1] = MathMax(-5.0, MathMin(5.0, qFast)); out[2] = MathMax(-5.0, MathMin(5.0, bSlow)); out[3] = MathMax(-5.0, MathMin(5.0, qSlow)); //--- DIVERGENCE: what the pair itself did over the slow window, minus what its two currencies //--- separately say it should have done. if(c1 > 0.0 && c0 > 0.0) { double own = MathLog(c1 / c0) * 100.0; out[4] = MathMax(-5.0, MathMin(5.0, own - (bSlow - qSlow))); } } else { //--- INDEX MODE (base == quote, e.g. SP500 -> USD/USD): the FX slots would be the same series //--- twice and a divergence of exactly zero information. Re-encoded (see the file header): //--- 0/2 = DENOMINATION currency strength (fast/slow). double dFast = (Strength(m_baseIdx, idx) - Strength(m_baseIdx, idx + CROSSASSET_FAST_BARS)) * 100.0; double dSlow = (Strength(m_baseIdx, idx) - Strength(m_baseIdx, idx + CROSSASSET_SLOW_BARS)) * 100.0; double rFast = 0.0, rSlow = 0.0; if(m_riskIdx >= 0) { rFast = (Strength(m_riskIdx, idx) - Strength(m_riskIdx, idx + CROSSASSET_FAST_BARS)) * 100.0; rSlow = (Strength(m_riskIdx, idx) - Strength(m_riskIdx, idx + CROSSASSET_SLOW_BARS)) * 100.0; } out[0] = MathMax(-5.0, MathMin(5.0, dFast)); out[1] = MathMax(-5.0, MathMin(5.0, rFast)); out[2] = MathMax(-5.0, MathMin(5.0, dSlow)); out[3] = MathMax(-5.0, MathMin(5.0, rSlow)); if(c1 > 0.0 && c0 > 0.0) { double own = MathLog(c1 / c0) * 100.0; out[4] = MathMax(-5.0, MathMin(5.0, own + dSlow)); } } //--- cross-sectional dispersion, averaged over the slow window: regime context that belongs to //--- the market as a whole rather than to either currency double disp = 0.0; for(int k = 0; k < CROSSASSET_SLOW_BARS; k++) disp += m_dispersion[idx + k]; out[5] = MathMax(0.0, MathMin(5.0, (disp / CROSSASSET_SLOW_BARS) * 1000.0)); return(true); } #endif // WARRIOR_CROSSASSET_MQH