forked from mnbvc188199/Warrior_EA
Four defects in 64c5dd5/1a05e63, found by review + a baseline compile. Goals 1-8 of that session are unchanged; this makes 6 and 8 actually reachable. 1. HEAD DID NOT COMPILE - 6 errors. CControlPanel::Minimize/Maximize were declared `virtual bool ... override`, but CAppDialog declares both as `virtual void` (Controls\Dialog.mqh). errors 265 + 404 on each, plus 151 on `bool ok = CAppDialog::Minimize()`. Return type is void now; there was never a success flag to forward. Verified: 0 errors, 0 warnings. 2. THE BACKFILL COULD NEVER ADVANCE, and neither could the OOS continual simulation (that one has been dead since it was written). Both are armed at the instant convergence is declared, and both advance only from inside Train(), one chunk per call. But ScheduleTrainingIfNeeded's only per-tick ArmStudyEvent site sits in the `else` of a branch taken whenever m_trainingComplete is set and m_trainRunActive is clear - which is exactly the state FinalizeTrainRun() leaves behind one line before they are armed. Train() was never called again, so the walks sat at their start index forever: no "simulation complete" line, and not one row written to the DB this feature exists to fill. Only a manual Resume/Retrain unstuck them. Both flags now keep the model schedulable. 3. IN AI_HYBRID - the mode this ships in - the backfill was never even armed. Ensemble members deploy at Train() ENTRY and return immediately (so no era is wasted), which skips the era-end block the backfill was started from. All four members were a no-op for a second, independent reason. Armed on the ensemble deploy path too, from m_resumeBars/m_resumeOosCutoff. 4. RE-RUNS DUPLICATED ROWS. RegisterSignal inserts unconditionally - no key, no duplicate check - and m_dbBackfillDone is in-memory, so every later attach that retrained to convergence wrote a second full set of rows for the same bars. The ranking would count one bar once per model that ever deployed, weighting superseded opinions as heavily as the live one. A .dbfill marker stamps the deployed era; written only on completion (an interrupted walk redoes itself rather than ranking a partial window) and deleted with the other sidecars on reset-weights. Also: WarmBlocking's timeout was silent, which restored the exact silent pin failure it was added to prevent - it now says so in the journal, and returns true for "no reference pairs to wait for" so the warning stays rare enough to be read. Not addressed, needs a decision: the backfill scores the OOS window with the checkpoint that was SELECTED as best on that same window, then writes those win rates into the table filter weights rank on - the selection set consumed twice, undiscounted, while the deploy gate right next to it applies a family-wise correction for exactly that effect. The rows are also simulated triple-barrier outcomes at today's spread sharing a table with realised fills. The completion log line now states both plainly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
640 lines
30 KiB
MQL5
640 lines
30 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);
|
|
//--- 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). If a
|
|
//--- reference pair is still mid-sync at that moment, Build() degrades gracefully and pins the
|
|
//--- SMALLER set it happened to get - the missing pair then never joins, even once it finishes
|
|
//--- syncing, because HasPinnedPairs() is true from then on. Waiting here up to timeoutMs gives
|
|
//--- every discovered pair a real chance to finish before that one-shot pin happens.
|
|
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. A symbol whose currency decomposition yields no
|
|
//--- reference pairs has no pin to protect, so reporting "timed out" there would fire the caller's
|
|
//--- missing-pair warning on every attach and train the operator to ignore it - which is precisely
|
|
//--- when the real one goes unread. 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. 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
|