forked from animatedread/Warrior_EA
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>
593 lines
28 KiB
MQL5
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
|