Warrior_EA/System/CrossAsset.mqh
AnimateDread 923addf574 feat: pin the cross-asset pair set train->serve + warm the sync at init
The reference-pair set was re-discovered from Market Watch on every
build, so adding or removing a terminal symbol silently changed what a
trained model's six cross-asset features meant - the last open
train/serve parity gap from the 2026-08-11 audit. The set a model's
FIRST successful build actually used is now stamped into its .cfg
(append-and-length-guard, adopt-don't-compare - the derived-barrier
pattern) and every later build constructs the panel from exactly that
list; a pinned pair that is temporarily unavailable is skipped, never
substituted.

Also warms SymbolSelect/SeriesInfo for every reference symbol at
InitNeuralNetwork, so the terminal's ~minute of async cross-symbol
download starts at init instead of when the first Build() trips over
an unselected symbol - the source of the startup 'only 0 usable
reference pairs' console failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 21:29:14 -04:00

593 lines
28 KiB
MQL5

//+------------------------------------------------------------------+
//| 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. |
//| |
//| INDEX MODE (base == quote). A CFD on an index or a commodity |
//| reports the same currency on both sides (SP500 -> USD/USD), so |
//| the FX encoding above degenerates: base and quote strength are |
//| the SAME series twice, and the divergence collapses to the |
//| symbol's own return - three of six slots wasted. For those |
//| symbols the panel re-encodes: the DENOMINATION currency's |
//| strength (a stronger USD mechanically pressures a USD-priced |
//| index), a RISK-PROXY currency's strength (JPY by preference - |
//| yen strength is the classic FX risk-off tell for equities), and |
//| divergence becomes the symbol's own move minus what the |
//| denomination currency alone implies. Same six slots, no width |
//| change; the fingerprint carries an :IDX2 tag so index-symbol |
//| models trained under the old degenerate encoding re-key. |
//| |
//| 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;
//--- 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. When set,
//--- Build() uses exactly this list instead of re-discovering Market Watch, so adding/removing a
//--- terminal symbol can no longer change what a trained model's features mean. 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[]);
string BaseCurrency(void) const { return(m_baseCcy); }
string QuoteCurrency(void) const { return(m_quoteCcy); }
int CurrencyCount(void) const { return(m_ccyCount); }
//--- 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);
};
//+------------------------------------------------------------------+
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
}
//+------------------------------------------------------------------+
//| 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. A fixed preference order, not |
//| "whatever slot came first": the pair set is discovered from |
//| Market Watch at every build, so slot order is an accident of |
//| discovery order - two builds with the same currencies must pick |
//| the SAME proxy or the feature changes meaning between training |
//| and serving. JPY leads because yen strength is the canonical FX |
//| risk-off reading for equity indices; the rest rank by how much |
//| of a safe-haven/beta signal each usually carries. |
//+------------------------------------------------------------------+
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. 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 = 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. Currencies are re-resolved from
//--- the symbol (a symbol property, stable); a pinned pair the terminal no longer knows resolves
//--- to empty and is skipped - degraded, logged below, never substituted.
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. 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++;
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. 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 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. 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.
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). A USD-priced index is mechanically
//--- pressured when USD strengthens - the denominator effect.
//--- 1/3 = RISK-PROXY currency strength (fast/slow), JPY by preference: yen strength is the
//--- canonical FX risk-off tell for equities. 0-fill when no proxy currency exists.
//--- 4 = own slow move minus what the denomination alone implies (-dSlow), i.e. own + dSlow:
//--- an index holding its level THROUGH a strengthening denomination is genuinely bid.
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