//+------------------------------------------------------------------+ //| 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. | //| | //| Motivation. Every feature the network sees today is some function | //| of one price series - returns, ranges, oscillators, cloud | //| distances, swing structure. Measured end to end, that family | //| carries no directional edge (research/test_classic.py, and the | //| mutual-information floor before it). A single series simply may | //| not contain the answer. EURUSD moving is a statement about EUR | //| and about USD, and which one moved is invisible from EURUSD | //| alone - but it is plainly visible if you also look at EURJPY, | //| GBPUSD and the rest. | //| | //| So this builds a CURRENCY STRENGTH INDEX. For each currency, the | //| index is the average log-return across every available pair | //| containing it, signed so that "up" always means that currency | //| strengthened. The traded symbol's own base and quote indices, | //| and the DIVERGENCE between the pair and what its two currencies | //| were separately doing, are the features. | //| | //| Cost discipline: the whole cross-asset panel is built ONCE per | //| training run into arrays indexed by the traded symbol's own bar | //| index. Calling iBarShift()/CopyClose() per bar per pair would be | //| ~8 currencies x N pairs x 178k bars of cross-symbol lookups. | //+------------------------------------------------------------------+ #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; //--- 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); 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[]); string BaseCurrency(void) const { return(m_baseCcy); } string QuoteCurrency(void) const { return(m_quoteCcy); } int CurrencyCount(void) const { return(m_ccyCount); } }; //+------------------------------------------------------------------+ 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) { } //+------------------------------------------------------------------+ 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); } //+------------------------------------------------------------------+ //| 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. XAUUSD -> XAU/USD, SP500 -> USD/USD-ish) but its //--- move is not a statement about the base CURRENCY, so folding it into a strength average //--- would inject the very single-series noise this panel exists to look past. 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? | //| | //| Cross-symbol access in MT5 is ASYNCHRONOUS: a Copy* call may | //| return partial data, or -1, simply because the terminal has not | //| finished building that series yet - with no error that | //| distinguishes it from "this symbol has no history at all". | //| Worse, SymbolIsSynchronized() (symbol-wide) and SERIES_SYNCHRONIZED| //| (this symbol AND this timeframe) can disagree, because the | //| terminal builds series on separate threads. Both are checked. | //| | //| Deliberately NON-BLOCKING - no retry loop, no Sleep. A pair that | //| is not ready is skipped for this build and picked up on a later | //| one; blocking here would stall OnTick for every unsynchronised | //| symbol in Market Watch. | //| | //| In the STRATEGY TESTER the agent loads auxiliary symbols from the | //| client terminal, NOT from the trade server - so a reference pair | //| whose history was never downloaded into the terminal is simply | //| absent, and no amount of waiting produces it. That is why a | //| failure here logs and degrades rather than retrying. | //+------------------------------------------------------------------+ 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 = -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(!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; 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. Matching "the last reference bar at or before this timestamp" is what makes //--- this robust to the tester's per-symbol tick sequences (bars genuinely do NOT open together //--- across symbols there, and on M1-M30 in "Open prices only" mode a non-chart symbol reads a //--- documented one bar stale). 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. Anything older than one //--- bar period is treated as no data rather than as a stale fill. 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++; } 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_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. Scale by 100 to put a typical H1 move near unity rather than //--- near 0.001, which would vanish into rounding against the ATR-normalised price features, //--- then clamp: the clamp is what stops a flash-crash bar from dominating a whole batch. 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. This is the one feature here that cannot be derived //--- from the traded series at all - it is only defined relative to the rest of the market, and //--- it is the panel's actual thesis: a pair that has NOT kept up with its own currencies is in //--- a different state from one that led them. double c1 = iClose(m_symbol, m_period, idx); double c0 = iClose(m_symbol, m_period, idx + CROSSASSET_SLOW_BARS); 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))); } //--- 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