fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| CrossAsset.mqh |
|
|
|
|
|
//| AnimateDread |
|
|
|
|
|
//| |
|
2026-08-22 00:30:14 -04:00
|
|
|
//| Information that is NOT a transform of the traded symbol's own |
|
|
|
|
|
//| OHLCV series: what every OTHER instrument was doing at the same |
|
|
|
|
|
//| moment. |
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#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;
|
2026-08-11 21:07:52 -04:00
|
|
|
//--- index mode only (m_baseIdx == m_quoteIdx): the risk-proxy currency's slot, -1 when none
|
|
|
|
|
int m_riskIdx;
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- Train->serve parity: the pair set a model was TRAINED on, adopted from the .cfg. A pinned
|
|
|
|
|
//--- pair that is temporarily unavailable is skipped for that build (degraded, same as before),
|
|
|
|
|
//--- never substituted.
|
2026-08-11 21:29:14 -04:00
|
|
|
string m_pinnedPairs[];
|
|
|
|
|
//--- the pairs the LAST successful Build actually used - what a fresh model pins to its .cfg
|
|
|
|
|
string m_usedPairsCsv;
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
//--- 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[]);
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
bool SeriesReady(string sym, ENUM_TIMEFRAMES tf, int need);
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
int CurrencySlot(string ccy);
|
2026-08-11 21:07:52 -04:00
|
|
|
int FindCurrency(string ccy) const;
|
|
|
|
|
int SelectRiskProxy(void) const;
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
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[]);
|
2026-08-11 21:29:14 -04:00
|
|
|
//--- 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);
|
2026-08-16 21:08:41 -04:00
|
|
|
//--- BLOCKING sibling of Warm(), called once from OnInit BEFORE any model exists (same fix as
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- 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).
|
2026-08-16 21:08:41 -04:00
|
|
|
bool WarmBlocking(ENUM_TIMEFRAMES period, int timeoutMs);
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
CCrossAssetPanel::CCrossAssetPanel(void) : m_symbol(""), m_period(PERIOD_CURRENT), m_bars(0),
|
2026-08-11 21:07:52 -04:00
|
|
|
m_ready(false), m_baseCcy(""), m_quoteCcy(""), m_ccyCount(0), m_baseIdx(-1), m_quoteIdx(-1),
|
|
|
|
|
m_riskIdx(-1)
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-11 21:29:14 -04:00
|
|
|
//| 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
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-16 21:08:41 -04:00
|
|
|
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];
|
|
|
|
|
}
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- NOTHING TO WAIT FOR is success, not failure. Only an expired wait with pairs still unsynced
|
|
|
|
|
//--- returns false.
|
2026-08-16 21:08:41 -04:00
|
|
|
else if(!DiscoverPairs(pairs, pbase, pquote))
|
fix: the DB backfill could never run, and HEAD did not compile
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>
2026-08-16 21:25:51 -04:00
|
|
|
return true;
|
2026-08-16 21:08:41 -04:00
|
|
|
int n = ArraySize(pairs);
|
|
|
|
|
if(n == 0)
|
fix: the DB backfill could never run, and HEAD did not compile
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>
2026-08-16 21:25:51 -04:00
|
|
|
return true;
|
2026-08-16 21:08:41 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-11 21:07:52 -04:00
|
|
|
//| 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);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| Index mode's risk-proxy currency. |
|
2026-08-11 21:07:52 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
//| 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
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- a metal reports a currency pair (e.g.
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| Is sym/tf genuinely ready to be read to `need` bars? |
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
//| 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;
|
2026-08-11 21:07:52 -04:00
|
|
|
m_baseIdx = m_quoteIdx = m_riskIdx = -1;
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
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[];
|
2026-08-11 21:29:14 -04:00
|
|
|
if(HasPinnedPairs())
|
|
|
|
|
{
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- 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.
|
2026-08-11 21:29:14 -04:00
|
|
|
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))
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
{
|
|
|
|
|
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;
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
int skippedUnsynced = 0;
|
2026-08-11 21:29:14 -04:00
|
|
|
string usedCsv = "";
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
for(int p = 0; p < ArraySize(pairs); p++)
|
|
|
|
|
{
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
if(!SeriesReady(pairs[p], period, bars))
|
|
|
|
|
{
|
|
|
|
|
skippedUnsynced++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
//--- 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);
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- Staleness cap. But an unbounded "at or before" would happily match a quote from three
|
|
|
|
|
//--- days ago across a market holiday and call it this bar's price.
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
int stale = (int)PeriodSeconds(period);
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
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--;
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
if(pt[cur] <= times[b] && (int)(times[b] - pt[cur]) <= stale)
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
{
|
|
|
|
|
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++;
|
2026-08-11 21:29:14 -04:00
|
|
|
usedCsv += (usedCsv == "" ? "" : ",") + pairs[p];
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
}
|
|
|
|
|
if(used < 2 || m_ccyCount <= 0)
|
|
|
|
|
{
|
feat(ai): cross-asset currency strength - the first feature not derived from one price series
Every feature the network sees today is a function of the traded symbol's own OHLCV:
returns, ranges, oscillators, cloud distances, swing structure. Measured end to end that
whole family sits at the noise floor (research/test_classic.py, and the mutual-information
verdict before it). EURUSD moving is a statement about EUR and about USD, and which one
moved is invisible from EURUSD alone - but plainly visible if you also look at EURJPY,
GBPUSD and the rest.
System\CrossAsset.mqh builds a currency-strength panel from the FX pairs in Market Watch:
per bar, each currency's index is the average log return across every available pair
containing it, signed so "up" always means that currency strengthened. Six features - base
and quote strength at 1 and 20 bars, the DIVERGENCE between the pair and what its two
currencies separately did, and the cross-sectional dispersion of currency moves as a
regime term. The divergence is the thesis: it is the one value here that cannot be derived
from the traded series at all, being defined only relative to the rest of the market.
Built ONCE per training run against the traded symbol's bar grid, not per bar - a per-bar
cross-symbol lookup would be pairs x 178k iBarShift calls.
Correctness work, all of it driven by what MT5 actually guarantees rather than by what the
API surface suggests:
- Alignment is by TIMESTAMP, never by index. Bars do not open together across symbols, and
in the tester each symbol gets its own generated tick sequence, so index k on GBPUSD and
index k on USDJPY are not the same instant. Each traded bar takes the last reference bar
at or BEFORE its timestamp - never after, which would be lookahead - and anything more
than one bar period stale is treated as absent rather than carried forward across a
holiday gap.
- SeriesReady() gates every pair on SymbolSelect + SymbolIsSynchronized + the PER-TIMEFRAME
SERIES_SYNCHRONIZED. The symbol-wide and per-timeframe flags can disagree because the
terminal builds series on separate threads, so checking only the first is not enough.
Non-blocking by design: an unready pair is skipped and picked up on a later build.
- Failure is never fatal. Fewer than two usable pairs logs why and every Features() call
0-fills, so a missing reference symbol costs the context block rather than the whole run.
Fingerprint: the flag goes in, the DISCOVERED REFERENCE SET does not. Which pairs exist in
Market Watch is a measured property of the terminal, exactly like the bar count the
existing comment warns about - keying the weights filename on it would orphan a trained
model the moment the user adds a symbol, silently, because a missing cache reads as a
normal first run.
Defaults ON, which re-keys existing databases on first run. That is intended: the input
vector genuinely changed shape.
Deliberately NOT built, having checked what the platform actually provides:
- swap/carry. SYMBOL_SWAP_LONG/SHORT have no history - "last values will be used for the
whole test period" - so a backtest over 2020-2026 applies 2026 carry to 2020 bars.
- signed order flow. TICK_FLAG_BUY/SELL and volume_real are empty on Forex; any feature
assuming trade direction would silently be all zeros.
- depth of market. Unavailable on retail FX symbols and never replayed in the tester.
- calendar actual-vs-forecast surprise. MqlCalendarValue.actual_value is the FINAL,
post-revision figure and the calendar keeps no as-of-release snapshot, so a surprise
feature for a 2019 bar is built from a number nobody had in 2019. Needs a live recorder,
not a historical read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:16:13 -04:00
|
|
|
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);
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
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);
|
|
|
|
|
}
|
2026-08-11 21:29:14 -04:00
|
|
|
m_usedPairsCsv = usedCsv;
|
2026-08-11 21:07:52 -04:00
|
|
|
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);
|
|
|
|
|
}
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
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,
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- symbol-independent range.
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
double c1 = iClose(m_symbol, m_period, idx);
|
|
|
|
|
double c0 = iClose(m_symbol, m_period, idx + CROSSASSET_SLOW_BARS);
|
2026-08-11 21:07:52 -04:00
|
|
|
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
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- separately say it should have done.
|
2026-08-11 21:07:52 -04:00
|
|
|
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
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
{
|
2026-08-11 21:07:52 -04:00
|
|
|
//--- 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):
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- 0/2 = DENOMINATION currency strength (fast/slow).
|
2026-08-11 21:07:52 -04:00
|
|
|
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));
|
|
|
|
|
}
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
}
|
|
|
|
|
//--- 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
|