DIRECTION IS NOT THERE, and this run is what establishes it. Three symbols:
raw ASYMMETRY clears on all three (p=0.0199 / 0.0050 / 0.0050)
norm ASYMMETRY collapses on all three (p=0.3433 / 0.5075 / 0.2736),
USDCAD landing BELOW its own null
RANGE control strengthens to 3-5x its null everywhere
Divide sigma out and the apparent directional signal vanishes entirely. What
cleared was volatility leaking through an unnormalised difference. Note this
would have passed any replication test: three instruments at p=0.005 is exactly
the evidence one would accept before committing to a rebuild, and the confound
reproduces perfectly. Replication was never going to catch it - only the
normalisation could.
Two defects of mine, both surfaced by the same run.
1. THE GEOMETRY DERIVATION WAS DIVERGING, NOT CONVERGING. It produced a
14.57*ATR stop and a 29.14*ATR target that only 5.7% of bars ever reach.
Excursions were measured over the barrier horizon; the horizon scales with
the target; the target is a quantile of the excursions - so target ->
horizon -> excursions -> target ran away, and "settled" only because the
horizon ladder caps at 384 bars. A saturated runaway, which the iteration
guard could not catch because it watches for OSCILLATION.
Fixed at the root: excursions now accumulate only over m_swingMedianBars -
the UNSCALED median ZigZag leg, a property of the instrument that owes
nothing to the barrier. The barrier walk still runs the full horizon,
because that is how long the trade is held; only the MEASUREMENT used to
size the barrier is confined to a geometry-independent window.
(The Min_Risk_Reward_Ratio warning fired correctly and is what flagged it -
the diagnostic worked while the derivation behind it did not.)
2. THE CONFOUND VERDICT WAS UNREACHABLE. `sizeCleared && !asymCleared` was
tested first and is true whenever size clears - i.e. always - so the branch
that NAMES the volatility confound never printed; all three symbols showed
the generic size-not-direction message instead. Verdict chain rewritten with
the specific case first, and the dangling elses my first patch introduced
removed.
FORCES A FULL RETRAIN (the excursion window changes every derived barrier).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
746 lines
44 KiB
MQL5
746 lines
44 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| Triple-barrier labelling and the async label-cache prebuild. |
|
|
//| |
|
|
//| PARTIAL IMPLEMENTATION FILE - not standalone. |
|
|
//| This holds CExpertSignalAIBase method BODIES only. The class |
|
|
//| declaration lives in Expert\ExpertSignalAIBase.mqh, which |
|
|
//| #includes this file at the bottom, after the declaration. Do not |
|
|
//| include it anywhere else and do not compile it on its own. |
|
|
//| |
|
|
//| Split out purely to make the 8216-line original navigable; the |
|
|
//| code inside was moved verbatim, not rewritten. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_AIBASE_LABELS_MQH
|
|
#define WARRIOR_AIBASE_LABELS_MQH
|
|
//+------------------------------------------------------------------+
|
|
//| (Re)sizes the label AND feature caches and clears them if `bars` |
|
|
//| (or the now-relative index frame) has changed since the last |
|
|
//| build - see the member declaration comments for why this is the |
|
|
//| correct invalidation trigger. Returns true if a rebuild happened. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::EnsureBarCachesCapacity(int bars)
|
|
{
|
|
if(bars == m_labelCacheBars && m_Time.GetData(0) == m_labelCacheAnchorTime)
|
|
return false;
|
|
ArrayResize(m_labelCacheBuy, bars);
|
|
ArrayResize(m_labelCacheSell, bars);
|
|
//--- Sized with the label caches they share a validity flag with, so the three can never disagree
|
|
//--- about how many bars they cover.
|
|
ArrayResize(m_excUpCache, bars);
|
|
ArrayResize(m_excDownCache, bars);
|
|
ArrayResize(m_labelCacheHasValue, bars);
|
|
ArrayInitialize(m_labelCacheHasValue, false);
|
|
ArrayResize(m_featureCache, bars * m_neuronsCount);
|
|
ArrayResize(m_featureCacheHasValue, bars);
|
|
ArrayResize(m_featureCacheValid, bars);
|
|
ArrayInitialize(m_featureCacheHasValue, false);
|
|
m_labelCacheBars = bars;
|
|
m_labelCacheAnchorTime = m_Time.GetData(0);
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Lazy cache-miss fallback for a bar the eager prebuild pass (see |
|
|
//| AdvanceBarrierLabelState()) didn't cover - e.g. a new candle that |
|
|
//| closed after prebuild already completed. Such a bar sits inside |
|
|
//| the unresolved horizon: its triple-barrier outcome needs |
|
|
//| m_barrierHorizonBars more closes before it is knowable at all. |
|
|
//| Rather than guess, this always labels Neutral; the sequential |
|
|
//| prebuild scan is what assigns Buy/Sell once the forward window |
|
|
//| this bar's verdict depends on has actually closed. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::ComputeLabelForBar(int i, int bars, bool &buy, bool &sell)
|
|
{
|
|
buy = false;
|
|
sell = false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| SL/TP ATR multiples for the triple-barrier label, taken from the |
|
|
//| EA's own SL_Mode/TP_Mode (m_sl_mode/m_tp_mode, protected members |
|
|
//| of CExpertSignalCustom, set in Warrior_EA.mq5's per-topology |
|
|
//| setup block). Using the traded values is the entire point: it is |
|
|
//| what makes the era line's dir-precision a real win rate instead |
|
|
//| of a proxy for one. |
|
|
//| |
|
|
//| The INTELLIGENT modes scale with AI confidence, which does not |
|
|
//| exist when a label is computed - and must not, or the target |
|
|
//| would depend on the model's own output and the whole thing would |
|
|
//| be circular. Both therefore fall back to their ZERO-CONFIDENCE |
|
|
//| base (the trade the EA would place knowing nothing), which is |
|
|
//| also the widest stop and tightest target either mode can pick, so |
|
|
//| the label is the conservative member of the family it stands for. |
|
|
//| TP_INTELLIGENT is risk-relative by design, so its multiple is |
|
|
//| expressed against the resolved stop rather than against ATR. |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| DERIVE THE BARRIER FROM WHAT PRICE ACTUALLY DOES, not from an |
|
|
//| enum. Reads the measured MFE/MAE distribution collected by the |
|
|
//| label prebuild and sets the ATR multiples from its quantiles. |
|
|
//| |
|
|
//| WHY THIS AND NOT THE GEOMETRY SCAN. The scan ranks candidate SL:TP |
|
|
//| pairings by how predictable their OUTCOME is, which is a question |
|
|
//| about direction - and direction is the one thing measured absent |
|
|
//| here (ASYMMETRY p=0.0846 on SP500 H1, against RANGE/UP/DOWN all at |
|
|
//| p=0.0050). That is why its winner fails its own gate on every run |
|
|
//| and why its "best" wanders 2:8 -> 3:8 -> 2:8 -> 2:4. Excursion |
|
|
//| SIZE, by contrast, clears at 4x its null. So derive the geometry |
|
|
//| from the quantity that is actually measurable. |
|
|
//| |
|
|
//| WHAT THIS DOES NOT DO: create expectancy. Under a driftless walk |
|
|
//| the probability of touching +k*ATR before -m*ATR is m/(m+k), which |
|
|
//| is ALSO the break-even win rate for that payoff - so no choice of |
|
|
//| geometry has an edge, and this one does not either. What it buys |
|
|
//| is a target that is actually reachable inside the horizon and a |
|
|
//| stop wide enough to survive ordinary noise, both read off the |
|
|
//| data instead of guessed. The reachability figures are printed so |
|
|
//| the choice can be audited rather than trusted. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::DeriveBarrierGeometry(void)
|
|
{
|
|
int bars = m_labelCacheBars;
|
|
double up[], dn[];
|
|
ArrayResize(up, bars);
|
|
ArrayResize(dn, bars);
|
|
int n = 0;
|
|
//--- IS region only, matching BuildMiSample: a geometry chosen with the holdout in view has used the
|
|
//--- holdout for selection, and it stops being a holdout.
|
|
int oosCutoff = (int)(MathMax(0, MathMin(100, m_oosSplitPct)) / 100.0
|
|
* MathMax(bars - MathMax(m_historyBars, 0), 0));
|
|
for(int i = MathMax(oosCutoff, 0); i < bars; i++)
|
|
{
|
|
if(i >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[i])
|
|
continue;
|
|
if(i >= ArraySize(m_excUpCache))
|
|
continue;
|
|
double u = m_excUpCache[i], d = m_excDownCache[i];
|
|
if(!MathIsValidNumber(u) || !MathIsValidNumber(d) || (u <= 0.0 && d <= 0.0))
|
|
continue; // unresolvable bar - see the same guard in BuildMiSample
|
|
up[n] = u;
|
|
dn[n] = d;
|
|
n++;
|
|
}
|
|
if(n < BARRIER_DERIVE_MIN_SAMPLES)
|
|
{
|
|
Print(ID + StringFormat(": barrier geometry NOT derived - only %d usable excursion samples "
|
|
"(need %d). Falling back to the configured %d:%d.", n,
|
|
BARRIER_DERIVE_MIN_SAMPLES, m_sl_mode, m_tp_mode));
|
|
return false;
|
|
}
|
|
ArrayResize(up, n);
|
|
ArrayResize(dn, n);
|
|
ArraySort(up);
|
|
ArraySort(dn);
|
|
//--- STOP from the ADVERSE distribution, TARGET from the FAVOURABLE one - each leg sized by the thing
|
|
//--- it actually has to survive or reach. The stop sits at a HIGH quantile of MAE so only the minority
|
|
//--- of bars whose adverse travel exceeds it ever reach it; the target at the MEDIAN of MFE so it is
|
|
//--- reached about half the time within the horizon. See BARRIER_SL_QUANTILE for why that quantile is
|
|
//--- 0.75 and not 0.25 - the first version had it backwards and the printed reachability caught it.
|
|
double slRaw = dn[(int)MathMin(BARRIER_SL_QUANTILE * n, n - 1)];
|
|
double tpRaw = up[(int)MathMin(BARRIER_TP_QUANTILE * n, n - 1)];
|
|
//--- Same floor a real order gets, applied before the ratio so the ratio is computed on the stop that
|
|
//--- will actually be used (the ordering bug that once let TP sit under minRR - see 168422f's note).
|
|
if(slRaw < MIN_SL_ATR_MULTIPLIER)
|
|
slRaw = MIN_SL_ATR_MULTIPLIER;
|
|
double minRR = (double)Min_Risk_Reward_Ratio;
|
|
bool rrForced = false;
|
|
if(minRR > 0.0 && tpRaw < minRR * slRaw)
|
|
{
|
|
tpRaw = minRR * slRaw;
|
|
rrForced = true;
|
|
}
|
|
//--- REACHABILITY, measured not assumed: what share of bars actually saw an excursion this big. This
|
|
//--- is the number that catches a target the horizon cannot deliver - the failure that shipped once
|
|
//--- already, where a clamped horizon quietly made every label "target within 128 bars".
|
|
int reachTp = 0, reachSl = 0;
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
if(up[i] >= tpRaw)
|
|
reachTp++;
|
|
if(dn[i] >= slRaw)
|
|
reachSl++;
|
|
}
|
|
double tpReach = 100.0 * reachTp / n;
|
|
double slReach = 100.0 * reachSl / n;
|
|
double breakeven = 100.0 * slRaw / (slRaw + tpRaw);
|
|
m_derivedSlMult = slRaw;
|
|
m_derivedTpMult = tpRaw;
|
|
m_geometryDerived = true;
|
|
Print(ID + StringFormat(": barrier geometry DERIVED from %d measured excursions - stop %.2f*ATR "
|
|
"(q%.0f of adverse travel), target %.2f*ATR (q%.0f of favourable)%s | reached "
|
|
"within the horizon: target on %.1f%% of bars, stop on %.1f%% | implied "
|
|
"break-even %.1f%%. Replaces the enum multiples; the grid those came from was "
|
|
"a set of guesses. This does NOT create expectancy - chance precision equals "
|
|
"break-even at every geometry - it makes the target reachable and the stop "
|
|
"survivable, both read off the data.",
|
|
//--- ORDER MATTERS AND WAS WRONG ONCE: the multiples and the quantile labels
|
|
//--- were swapped, so the log read "stop 25.00*ATR (q3 ...)" - printing the
|
|
//--- quantile percentage as the multiple and the multiple as the quantile.
|
|
//--- 25*ATR is absurd on its face, which is the only reason it was caught.
|
|
n, m_derivedSlMult, 100.0 * BARRIER_SL_QUANTILE, m_derivedTpMult,
|
|
100.0 * BARRIER_TP_QUANTILE,
|
|
(rrForced ? " [target RAISED to meet Min_Risk_Reward_Ratio]" : ""),
|
|
tpReach, slReach, breakeven));
|
|
if(rrForced && tpReach < BARRIER_MIN_TP_REACH_PCT)
|
|
Print(ID + StringFormat(": WARNING - Min_Risk_Reward_Ratio forced the target to %.2f*ATR, which only "
|
|
"%.1f%% of bars ever reach inside the horizon. The reward:risk floor is "
|
|
"asking for a move this market rarely makes, so most trades will resolve at "
|
|
"the stop or time out. Lower the ratio or accept the hit rate - this is the "
|
|
"same collision that once rejected 100%% of setups.",
|
|
m_derivedTpMult, tpReach));
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::BarrierMultiples(double &slMult, double &tpMult)
|
|
{
|
|
//--- Scan override (ReportBarrierGeometryScan). Both must be positive or neither applies, so a half-set
|
|
//--- pair can never silently relabel a live run. Restored to 0 by the scan before it returns; nothing
|
|
//--- else writes these, and no persisted state is keyed on them.
|
|
if(m_barrierScanSlMult > 0.0 && m_barrierScanTpMult > 0.0)
|
|
{
|
|
slMult = m_barrierScanSlMult;
|
|
tpMult = m_barrierScanTpMult;
|
|
return;
|
|
}
|
|
//--- DERIVED geometry wins over the mode constants. Set once from the measured excursion distribution
|
|
//--- (DeriveBarrierGeometry) and then pinned in the .cfg, so a trained model keeps the barriers it
|
|
//--- learned. Below the scan override deliberately: the scan is exploring hypothetical geometries and
|
|
//--- must still be able to impose one.
|
|
if(m_geometryDerived && m_derivedSlMult > 0.0 && m_derivedTpMult > 0.0)
|
|
{
|
|
slMult = m_derivedSlMult;
|
|
tpMult = m_derivedTpMult;
|
|
return;
|
|
}
|
|
slMult = (m_sl_mode == SL_INTELLIGENT_MODE) ? SL_INTELLIGENT_BASE_MULT : (double)m_sl_mode;
|
|
//--- Same floor OpenLongParams/OpenShortParams apply before sizing anything off the stop, reproduced
|
|
//--- here so the label's risk leg cannot be tighter than the one a real order would receive.
|
|
if(slMult < MIN_SL_ATR_MULTIPLIER)
|
|
slMult = MIN_SL_ATR_MULTIPLIER;
|
|
tpMult = (m_tp_mode == TP_INTELLIGENT_MODE) ? (TP_INTELLIGENT_BASE_RR * slMult) : (double)m_tp_mode;
|
|
if(tpMult <= 0.0)
|
|
{
|
|
//--- UNREACHABLE via the Inputs tab: ValidateBarrierInputs() (Warrior_EA.mq5) refuses to start on
|
|
//--- any value that is not an enum member. It is kept, and made LOUD, because the silent version of
|
|
//--- this line is what let a stale TP_PREV_SWING (-101) train four topologies for ~250 eras on a
|
|
//--- 1:1 barrier while the log cheerfully reported "target 1.00*ATR" as if that were configured.
|
|
//--- A fallback that cannot announce itself is indistinguishable from correct behaviour.
|
|
if(!m_barrierFallbackWarned)
|
|
{
|
|
m_barrierFallbackWarned = true;
|
|
Print(ID + ": ERROR - take-profit mode " + IntegerToString(m_tp_mode) + " is not a valid ATR "
|
|
"multiple; the barrier label is falling back to " + DoubleToString(slMult, 2) + "*ATR (1:1). "
|
|
"This should have been caught at init - the model being trained does NOT match the "
|
|
"configured strategy.");
|
|
}
|
|
tpMult = slMult;
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| TRIPLE-BARRIER LABEL for one bar (Lopez de Prado ch. 3). See the |
|
|
//| BARRIER_TIE_GOES_TO_STOP block in Expert\ExpertSignalAIBase.mqh |
|
|
//| for why this replaced the exact-pivot ZigZag target. |
|
|
//| |
|
|
//| Hypothetical entry at bar `idx`'s CLOSE - the same instant the |
|
|
//| feature window ends, so the label answers exactly the question |
|
|
//| the deployed model is asked live: "from what I can see right now, |
|
|
//| does a trade placed here reach its target before its stop?" |
|
|
//| |
|
|
//| Costs are charged. MT5 bar series are BID, so a long fills at ask |
|
|
//| (close + spread) and exits at bid, while a short fills at bid and |
|
|
//| buys back at ask - both legs shifted so the returned outcome is a |
|
|
//| NET result. Spread is taken as the symbol's current value, held |
|
|
//| constant across history: MT5's standard timeseries carries no |
|
|
//| per-bar spread, and a label that ignored the cost entirely would |
|
|
//| report a win rate the account cannot reproduce. |
|
|
//| |
|
|
//| Walks forward in time (toward index 0) for m_barrierHorizonBars. |
|
|
//| Ties inside one bar resolve to the STOP - OHLC cannot order two |
|
|
//| touches within a bar, and the optimistic reading is how a |
|
|
//| backtested edge becomes a live loss. |
|
|
//+------------------------------------------------------------------+
|
|
ENUM_SIGNAL CExpertSignalAIBase::TripleBarrierLabel(int idx)
|
|
{
|
|
//--- CLEARED FIRST, ahead of every early return below. These are published to the caller the way
|
|
//--- m_lastBarrierTimedOut is, and an unresolvable bar that returned before touching them would leave
|
|
//--- the PREVIOUS bar's excursions in place for AdvanceBarrierLabelState to cache against this index -
|
|
//--- one bar's outcome filed under another's, which is exactly the kind of silent contamination the
|
|
//--- excursion measurement is being built to avoid.
|
|
m_lastExcUp = 0.0;
|
|
m_lastExcDown = 0.0;
|
|
double atr = m_ATR.Main(idx);
|
|
if(!MathIsValidNumber(atr) || atr <= 0.0)
|
|
return Neutral; // no volatility scale yet - unresolvable, same practical answer as "no setup"
|
|
double entry = m_Close.GetData(idx);
|
|
if(!MathIsValidNumber(entry) || entry <= 0.0)
|
|
return Neutral;
|
|
double slMult, tpMult;
|
|
BarrierMultiples(slMult, tpMult);
|
|
double risk = slMult * atr;
|
|
double reward = tpMult * atr;
|
|
//--- Round-trip cost, in price. Both sides pay it once.
|
|
double spread = (double)m_symbol.Spread() * m_symbol.Point();
|
|
if(!MathIsValidNumber(spread) || spread < 0.0)
|
|
spread = 0.0;
|
|
//--- Barrier levels expressed in BID terms, which is what m_High/m_Low carry.
|
|
//--- Long fills at close+spread: target needs bid >= fill+reward, stop trips at bid <= fill-risk.
|
|
//--- Short fills at close: target needs bid <= close-reward-spread (it buys back at ask),
|
|
//--- stop trips at bid >= close+risk-spread.
|
|
double longTp = entry + spread + reward;
|
|
double longSl = entry + spread - risk;
|
|
double shortTp = entry - reward - spread;
|
|
double shortSl = entry + risk - spread;
|
|
bool longWon = false, longLost = false, shortWon = false, shortLost = false;
|
|
m_lastBarrierTimedOut = false;
|
|
//--- Excursion accumulators. Deliberately NOT stopped when a barrier trips: they describe how far
|
|
//--- price travelled over the whole horizon, which is the question a predicted SL/TP needs answered,
|
|
//--- whereas the barriers describe what a trade with THIS geometry would have collected. Truncating
|
|
//--- them at the first touch would bake the current SL/TP back into the measurement of whether a
|
|
//--- different SL/TP is learnable - the circularity the whole exercise is trying to escape.
|
|
double maxHigh = -DBL_MAX, minLow = DBL_MAX; // published values already cleared at the top
|
|
//--- Never longer than the horizon actually walked, so the window cannot claim bars the loop below
|
|
//--- does not visit; falls back to the horizon before the swing median has been measured.
|
|
int excWindow = (m_swingMedianBars > 0)
|
|
? (int)MathMin(m_swingMedianBars, MathMax(m_barrierHorizonBars, 1))
|
|
: MathMax(m_barrierHorizonBars, 1);
|
|
int last = idx - MathMax(m_barrierHorizonBars, 1);
|
|
if(last < 0)
|
|
last = 0;
|
|
for(int t = idx - 1; t >= last; t--)
|
|
{
|
|
double hi = m_High.GetData(t);
|
|
double lo = m_Low.GetData(t);
|
|
if(!MathIsValidNumber(hi) || !MathIsValidNumber(lo) || hi == EMPTY_VALUE || lo == EMPTY_VALUE)
|
|
break; // ran off loaded history - whatever resolved so far stands, the rest times out
|
|
//--- Excursions accumulate only over the REFERENCE WINDOW, not the whole barrier horizon - see
|
|
//--- m_swingMedianBars. The barrier walk below still runs the full horizon, because that is how
|
|
//--- long the trade is actually held; only the MEASUREMENT used to size the barrier is confined to
|
|
//--- a window that does not depend on the barrier.
|
|
if(idx - t <= excWindow)
|
|
{
|
|
if(hi > maxHigh)
|
|
maxHigh = hi;
|
|
if(lo < minLow)
|
|
minLow = lo;
|
|
}
|
|
//--- Stop tested FIRST on each side, so a bar that spans both barriers is scored as the loss.
|
|
if(!longWon && !longLost)
|
|
{
|
|
if(lo <= longSl)
|
|
longLost = true;
|
|
else if(hi >= longTp)
|
|
longWon = true;
|
|
}
|
|
if(!shortWon && !shortLost)
|
|
{
|
|
if(hi >= shortSl)
|
|
shortLost = true;
|
|
else if(lo <= shortTp)
|
|
shortWon = true;
|
|
}
|
|
//--- The early-out that used to sit here (both sides resolved -> break) is GONE, because the
|
|
//--- excursion accumulators above must see the whole horizon and it would have truncated them at
|
|
//--- whichever bar happened to trip the last barrier - making the measured excursion a function of
|
|
//--- the current SL/TP, which is exactly the circularity being escaped. The loop was already
|
|
//--- bounded by m_barrierHorizonBars, so the worst case is unchanged and only the average moves.
|
|
}
|
|
if(maxHigh > -DBL_MAX && minLow < DBL_MAX)
|
|
{
|
|
//--- Same spread convention as the barriers: a long fills at close+spread, so its favourable
|
|
//--- excursion is measured from that fill and its adverse excursion likewise. Clamped at zero -
|
|
//--- a horizon whose every high sits below the fill has no favourable excursion, not a negative one.
|
|
m_lastExcUp = MathMax((maxHigh - (entry + spread)) / atr, 0.0);
|
|
m_lastExcDown = MathMax(((entry + spread) - minLow) / atr, 0.0);
|
|
}
|
|
//--- Mutually exclusive whenever reward >= risk, which every shipped SL/TP pairing satisfies. The
|
|
//--- both-won branch is unreachable there but costs one comparison and keeps a degenerate custom
|
|
//--- configuration (TP tighter than SL) from silently producing two contradictory positives.
|
|
if(longWon && !shortWon)
|
|
return Buy;
|
|
if(shortWon && !longWon)
|
|
return Sell;
|
|
//--- Neither side resolved AT ALL = the vertical barrier is what ended it. Recorded separately from a
|
|
//--- stop-out because only this outcome says the horizon is too short - see m_lastBarrierTimedOut.
|
|
m_lastBarrierTimedOut = (!longWon && !longLost && !shortWon && !shortLost);
|
|
return Neutral; // timed out, stopped out, or an ambiguous config - no tradeable edge at this bar
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Median distance in bars between consecutive confirmed ZigZag |
|
|
//| pivots - this symbol/timeframe's own swing horizon, and what the |
|
|
//| vertical barrier is set to. Snapped to a coarse ladder so the |
|
|
//| estimate has to move ~30% to change the answer; see the |
|
|
//| BARRIER_HORIZON_* constants for why the quantization matters more |
|
|
//| than the precision (an unquantized horizon that drifted as |
|
|
//| history downloaded would relabel a partly-trained model's |
|
|
//| targets mid-run). |
|
|
//| |
|
|
//| Reads only pivots old enough to be non-repainting, for the same |
|
|
//| reason every other ZigZag read in this class does. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ComputeBarrierHorizonBars(int bars)
|
|
{
|
|
int ladder[BARRIER_HORIZON_LADDER_COUNT] = { 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384 };
|
|
int gaps[];
|
|
ArrayResize(gaps, 0);
|
|
int prevPivot = -1;
|
|
int scanned = 0;
|
|
//--- Oldest-to-newest is irrelevant here (a median has no order dependence), so scan newest-first from
|
|
//--- the first non-repainting bar and stop at the history edge.
|
|
for(int p = MathMax(m_swingConfirmationBars, 1); p < bars && scanned < SWING_SCAN_CAP_BARS * 4; p++, scanned++)
|
|
{
|
|
if(m_Open.GetData(p) == EMPTY_VALUE)
|
|
break;
|
|
if(m_ADZigZag.GetData(0, p) == 0.0)
|
|
continue;
|
|
if(prevPivot >= 0)
|
|
{
|
|
int gap = p - prevPivot;
|
|
if(gap > 0)
|
|
{
|
|
int n = ArraySize(gaps);
|
|
ArrayResize(gaps, n + 1);
|
|
gaps[n] = gap;
|
|
}
|
|
}
|
|
prevPivot = p;
|
|
}
|
|
int count = ArraySize(gaps);
|
|
double swingMedian = BARRIER_HORIZON_FALLBACK;
|
|
if(count >= BARRIER_HORIZON_MIN_SAMPLES)
|
|
{
|
|
ArraySort(gaps);
|
|
swingMedian = gaps[count / 2];
|
|
}
|
|
else
|
|
Print(ID + ": barrier horizon - only " + IntegerToString(count) + " confirmed ZigZag legs available (need " +
|
|
IntegerToString(BARRIER_HORIZON_MIN_SAMPLES) + "), falling back to " +
|
|
IntegerToString(BARRIER_HORIZON_FALLBACK) + " bars");
|
|
//--- SCALE BY THE BARRIER GEOMETRY. The swing median alone measures how long a ~1 ATR move takes on
|
|
//--- this instrument; it says nothing about how long the CONFIGURED barrier takes to resolve, and the
|
|
//--- first version of this function ignored that entirely.
|
|
//--- For a driftless random walk leaving the band [-m*ATR, +k*ATR], the expected first-passage time is
|
|
//--- proportional to m*k. So a 1:3 barrier takes ~3x as long to resolve as a 1:1 one, and a horizon
|
|
//--- tuned for 1:1 applied to 1:3 would time out most trades - pushing Neutral straight back up and
|
|
//--- re-creating the imbalance the relabel exists to remove.
|
|
//--- Calibrated against a real measurement rather than assumed: the 2026-08-01 run resolved at m=k=1
|
|
//--- with a 12-bar horizon and only 16.7% timeouts, so the swing median IS the right scale at m*k=1.
|
|
//--- Multiplying by m*k carries that calibration to every other barrier (1:3 -> 36, snapping to 32).
|
|
double slMult, tpMult;
|
|
BarrierMultiples(slMult, tpMult);
|
|
//--- THE EXCURSION REFERENCE WINDOW, published UNSCALED. This is a property of the instrument (how
|
|
//--- long its typical swing leg lasts) and owes nothing to the barrier, which is exactly what makes it
|
|
//--- usable for sizing the barrier. Sizing a stop off travel measured over the SCALED horizon below
|
|
//--- is circular: horizon grows with the target, excursions grow with the horizon, the target is a
|
|
//--- quantile of the excursions - so target -> horizon -> excursions -> target diverges. Measured
|
|
//--- 2026-08-07 on EURUSD/USDCAD: it ran away to a 14-15*ATR stop and a 29-31*ATR target that only
|
|
//--- 5.7-7.2% of bars ever reached, and "converged" solely because the ladder caps at 384 bars. A
|
|
//--- saturated runaway, not a fixed point - which is why the iteration guard, watching for
|
|
//--- oscillation, did not catch it.
|
|
m_swingMedianBars = (int)MathMax(MathRound(swingMedian), 1);
|
|
int raw = (int)MathRound(swingMedian * slMult * tpMult);
|
|
//--- CLAMPED means the barrier this geometry describes needs MORE time than the ceiling allows, so the
|
|
//--- label stops being "does the target come before the stop" and quietly becomes "does the target come
|
|
//--- within BARRIER_HORIZON_MAX bars". The deployed EA has no such bar limit - it holds until SL or TP -
|
|
//--- so a clamped label trains the model on a question the strategy never asks, and the unresolved
|
|
//--- remainder all lands in Neutral. Recorded rather than merely clamped because the geometry scan must
|
|
//--- be able to disqualify these: they LOOK informative precisely because a Neutral-dominated label has
|
|
//--- little entropy left to explain.
|
|
m_barrierHorizonClamped = (raw > BARRIER_HORIZON_MAX);
|
|
if(raw < BARRIER_HORIZON_MIN)
|
|
raw = BARRIER_HORIZON_MIN;
|
|
if(raw > BARRIER_HORIZON_MAX)
|
|
raw = BARRIER_HORIZON_MAX;
|
|
//--- Snap DOWN to the ladder, matching ComputeFirstLayerWidth()'s direction: a horizon shorter than
|
|
//--- measured makes the label stricter (more Neutral), never more permissive.
|
|
int snapped = ladder[0];
|
|
for(int k = 0; k < BARRIER_HORIZON_LADDER_COUNT; k++)
|
|
if(ladder[k] <= raw)
|
|
snapped = ladder[k];
|
|
return snapped;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Resolves m_barrierHorizonBars once per process and logs the whole |
|
|
//| label definition. Called from BOTH the training prebuild and the |
|
|
//| deployed inference path - see the declaration for why a deployed |
|
|
//| model that skipped this would silently learn online from bars |
|
|
//| whose barriers had not resolved. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::EnsureBarrierHorizon(int bars)
|
|
{
|
|
if(m_barrierHorizonResolved)
|
|
return;
|
|
m_barrierHorizonBars = ComputeBarrierHorizonBars(bars);
|
|
m_barrierHorizonResolved = true;
|
|
double slMultLog, tpMultLog;
|
|
BarrierMultiples(slMultLog, tpMultLog);
|
|
Print(ID + ": triple-barrier labels - stop " + DoubleToString(slMultLog, 2) + "*ATR, target " +
|
|
DoubleToString(tpMultLog, 2) + "*ATR, horizon " + IntegerToString(m_barrierHorizonBars) +
|
|
" bars (median confirmed ZigZag leg, snapped) | spread charged " +
|
|
IntegerToString(m_symbol.Spread()) + " points | intrabar ties score as the STOP");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Resolves the triple-barrier label for whichever candidate bar is |
|
|
//| exactly m_barrierHorizonBars behind the one being visited - i.e. |
|
|
//| the newest bar whose outcome is now fully knowable. Mirrors the |
|
|
//| shape of the ZigZag-confirmation scan this replaced, with the |
|
|
//| lookahead depth changed from "how long until a pivot stops |
|
|
//| repainting" to "how long until the trade resolves". |
|
|
//| |
|
|
//| Unlike the ZigZag version, a bar's verdict here is FINAL the |
|
|
//| moment it is computed: the barrier outcome depends only on price |
|
|
//| within a fixed forward window, so nothing later can revise it. |
|
|
//| That is what lets the widening/re-spreading pass this file used |
|
|
//| to need disappear entirely. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::AdvanceBarrierLabelState(int i, int bars)
|
|
{
|
|
int idx = i + MathMax(m_barrierHorizonBars, 1);
|
|
if(idx >= bars || m_labelCacheHasValue[idx])
|
|
return;
|
|
ENUM_SIGNAL verdict = TripleBarrierLabel(idx);
|
|
if(verdict == Neutral && m_lastBarrierTimedOut)
|
|
m_labelPrebuildTimeoutCount++;
|
|
m_labelCacheBuy[idx] = (verdict == Buy);
|
|
m_labelCacheSell[idx] = (verdict == Sell);
|
|
//--- Stored under the SAME validity flag as the label, set last so no reader can see one without the
|
|
//--- other. TripleBarrierLabel() publishes these for the bar it just walked.
|
|
if(idx < ArraySize(m_excUpCache))
|
|
{
|
|
m_excUpCache[idx] = m_lastExcUp;
|
|
m_excDownCache[idx] = m_lastExcDown;
|
|
}
|
|
m_labelCacheHasValue[idx] = true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Nearest confirmed ZigZag pivot at fromIdx or older (now-relative |
|
|
//| index, so "older" means scanning with INCREASING p - see this |
|
|
//| file's now-relative-index convention, same as AdvanceZigZagLabel- |
|
|
//| State() above). Capped at SWING_SCAN_CAP_BARS so a long quiet |
|
|
//| stretch with no qualifying pivot can't turn this into an unbounded |
|
|
//| scan; returns false (no pivot found) rather than looping forever |
|
|
//| if the cap is hit or history runs out first. |
|
|
//| |
|
|
//| Caller's responsibility, not this method's: applying the |
|
|
//| m_swingConfirmationBars repainting embargo to fromIdx before |
|
|
//| calling. This method itself just finds the nearest nonzero |
|
|
//| ADZigZag buffer entry at/after whatever index it's given - it has |
|
|
//| no opinion on whether that index is safe to read yet. The ONE |
|
|
//| caller that needs the embargo (BufferTempDataCompute()'s |
|
|
//| m_useSwingContext block, looking up "the pivot as of THIS bar") |
|
|
//| applies it before the first call; the second call in that same |
|
|
//| block (finding the PRIOR completed leg, starting from pivotIdx+1) |
|
|
//| doesn't need to re-apply it - anything at or before an already- |
|
|
//| confirmed pivot is necessarily even older, hence already confirmed |
|
|
//| too. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::FindConfirmedZigZagPivot(int fromIdx, int &pivotIdx, double &pivotPrice, bool &pivotIsLow)
|
|
{
|
|
for(int p = MathMax(fromIdx, 0); p < fromIdx + SWING_SCAN_CAP_BARS; p++)
|
|
{
|
|
if(m_Open.GetData(p) == EMPTY_VALUE)
|
|
return false; // ran off the end of available history
|
|
double zz = m_ADZigZag.GetData(0, p);
|
|
if(zz == 0.0)
|
|
continue;
|
|
pivotIdx = p;
|
|
pivotPrice = zz;
|
|
pivotIsLow = (zz <= m_Low.GetData(p) + _Point);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Kicks off the one-time eager label-cache pre-build for a fresh |
|
|
//| start (see m_labelCachePrebuilt's declaration comment). Computes |
|
|
//| the bar count/OOS split exactly as Train()'s era-start block |
|
|
//| would, then arms AdvanceLabelCachePrebuild() to do the actual |
|
|
//| chunked scan on this and subsequent Train() calls. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::StartLabelCachePrebuild(void)
|
|
{
|
|
int barsNow = (int)MathMin(Bars(m_symbol.Name(), PERIOD_CURRENT, dtStudied, TimeCurrent()) + m_historyBars, Bars(m_symbol.Name(), PERIOD_CURRENT));
|
|
if(!ResizeBuffers(barsNow) || !RefreshData())
|
|
return; // couldn't prep buffers yet - m_labelCachePrebuilt stays false, retried next call
|
|
EnsureBarCachesCapacity(barsNow);
|
|
//--- Settle the vertical barrier BEFORE the first label is computed. Derived once per process and then
|
|
//--- held: AdvanceBarrierLabelState() indexes off it, so a value that moved mid-scan would leave the
|
|
//--- cache holding labels from two different rules.
|
|
EnsureBarrierHorizon(barsNow);
|
|
int totalIter = (int)MathMax(barsNow - MathMax(m_historyBars, 0), 0);
|
|
m_labelPrebuildBars = barsNow;
|
|
m_labelPrebuildOosCutoff = (int)(MathMax(0, MathMin(100, m_oosSplitPct)) / 100.0 * totalIter);
|
|
m_labelPrebuildIndex = (int)(barsNow - MathMax(m_historyBars, 0) - 1);
|
|
m_labelPrebuildBuyCount = 0;
|
|
m_labelPrebuildSellCount = 0;
|
|
m_labelPrebuildNeutralCount = 0;
|
|
m_labelPrebuildTimeoutCount = 0;
|
|
m_labelPrebuildActive = true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Advances the eager label-cache pre-build by up to a time budget, |
|
|
//| then yields (same chunking pattern as the era loop). Mirrors the |
|
|
//| era loop's own labeling eligibility gate (minus the dPrevSignal |
|
|
//| check, meaningless pre-first-feedForward). On completion, seeds |
|
|
//| m_prevEraTrueBuyCount/Sell/Neutral from the upfront IS-only tally |
|
|
//| so era 0's class priors are measured, not empty. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::AdvanceLabelCachePrebuild(void)
|
|
{
|
|
const uint PREBUILD_TIME_BUDGET_MS = 80;
|
|
uint chunkStartTick = GetTickCount();
|
|
int i;
|
|
for(i = m_labelPrebuildIndex; i >= 2; i--)
|
|
{
|
|
if(GetTickCount() - chunkStartTick >= PREBUILD_TIME_BUDGET_MS)
|
|
{
|
|
m_labelPrebuildIndex = i;
|
|
return;
|
|
}
|
|
if(!(i < (int)(m_labelPrebuildBars - MathMax(m_historyBars, 0) - 1) && m_Time.GetData(i) > dtStudied))
|
|
continue;
|
|
// A barrier label needs m_barrierHorizonBars of FUTURE (lower-index) bars to resolve, so visiting
|
|
// bar i settles the label for bar i+horizon - see AdvanceBarrierLabelState(). Unlike the ZigZag
|
|
// scan this replaced, each verdict is final when written: the outcome depends only on price inside
|
|
// a fixed forward window, so no later iteration can revise it and there is no widening/re-spread
|
|
// pass to run afterwards. The tally still happens in one pass at the end, purely because the loop
|
|
// above is chunked across Train() calls and may resume mid-scan.
|
|
if(!m_labelCacheHasValue[i])
|
|
AdvanceBarrierLabelState(i, m_labelPrebuildBars);
|
|
}
|
|
//--- Final tally pass (IS-only, matches isOOS = (i < oosCutoff) used by the era loop).
|
|
for(i = m_labelPrebuildBars - MathMax(m_historyBars, 0) - 1; i >= MathMax(2, m_labelPrebuildOosCutoff); i--)
|
|
{
|
|
if(!m_labelCacheHasValue[i])
|
|
continue; // e.g. bar was outside the dtStudied/window-edge eligibility gate above
|
|
if(m_labelCacheBuy[i])
|
|
m_labelPrebuildBuyCount++;
|
|
else
|
|
if(m_labelCacheSell[i])
|
|
m_labelPrebuildSellCount++;
|
|
else
|
|
m_labelPrebuildNeutralCount++;
|
|
}
|
|
//--- Prebuild complete - seed era 0's class base rates from the real upfront tally instead of leaving
|
|
//--- UpdateClassPriors() nothing to measure (see m_prevEraTrueBuyCount's declaration comment).
|
|
//--- Consumed (and cleared) by Train()'s era-start block on era 0 specifically - m_prebuildSeedPending.
|
|
m_prevEraTrueBuyCount = m_labelPrebuildBuyCount;
|
|
m_prevEraTrueSellCount = m_labelPrebuildSellCount;
|
|
m_prevEraTrueNeutralCount = m_labelPrebuildNeutralCount;
|
|
m_prebuildSeedPending = true;
|
|
m_labelCachePrebuilt = true;
|
|
m_labelPrebuildActive = false;
|
|
//--- Measured-imbalance visibility. This line used to also report "reps up to Nx (M% parity)" and
|
|
//--- "(seeding era 0's class-balance oversampling)" - describing an oversampling pass that the
|
|
//--- logit-adjusted loss had already disabled, and which no longer exists at all since 2026-07-31.
|
|
//--- It was pure fiction in every shipped run, and convincing enough to send a diagnosis down the
|
|
//--- wrong path. A log line must describe what the code DID, not what some earlier version would
|
|
//--- have done: report the measured distribution, which is real and useful, and nothing else.
|
|
int prebuildMinDir = (int)MathMin(m_labelPrebuildBuyCount, m_labelPrebuildSellCount);
|
|
int prebuildMaxCls = (int)MathMax(m_labelPrebuildNeutralCount, MathMax(m_labelPrebuildBuyCount, m_labelPrebuildSellCount));
|
|
string prebuildRatioInfo = (prebuildMinDir > 0 && prebuildMaxCls > 0)
|
|
? " | measured imbalance ~" + DoubleToString((double)prebuildMaxCls / prebuildMinDir, 1) + ":1"
|
|
: " | measured imbalance n/a (a directional class has no labeled bars in this window)";
|
|
//--- These three counts are now WIN / LOSS-or-timeout counts under the EA's real stop and target, not
|
|
//--- pivot-spotting counts, so the Buy+Sell share here IS the fraction of bars offering a tradeable
|
|
//--- setup - and the era line's dir-precision against it is a win rate. This is the number that
|
|
//--- decides whether LogitAdjustTau still has a job: at a near-balanced split the log-prior spread
|
|
//--- collapses and the correction (plus its range cap, and the SoftMax port behind it) is redundant.
|
|
int prebuildTotal = m_labelPrebuildBuyCount + m_labelPrebuildSellCount + m_labelPrebuildNeutralCount;
|
|
string prebuildShare = (prebuildTotal > 0)
|
|
? " | share Buy " + DoubleToString(100.0 * m_labelPrebuildBuyCount / prebuildTotal, 1) +
|
|
"% Sell " + DoubleToString(100.0 * m_labelPrebuildSellCount / prebuildTotal, 1) +
|
|
"% Neutral " + DoubleToString(100.0 * m_labelPrebuildNeutralCount / prebuildTotal, 1) + "%"
|
|
: "";
|
|
Print(ID + ": label cache pre-built - IS true-label distribution -> Buy: " + IntegerToString(m_labelPrebuildBuyCount) +
|
|
" | Sell: " + IntegerToString(m_labelPrebuildSellCount) + " | Neutral: " + IntegerToString(m_labelPrebuildNeutralCount) +
|
|
prebuildRatioInfo + prebuildShare +
|
|
" | of which timed out (horizon too short?) " + IntegerToString(m_labelPrebuildTimeoutCount) +
|
|
(m_labelPrebuildNeutralCount > 0
|
|
? " = " + DoubleToString(100.0 * m_labelPrebuildTimeoutCount / m_labelPrebuildNeutralCount, 1) + "% of Neutral"
|
|
: "") +
|
|
(m_eraCount == 0 ? " (seeding era 0 - triple-barrier targets, so Buy/Sell mean 'target hit before stop')"
|
|
: " (mid-run rebuild after new-bar cache invalidation - era " + IntegerToString(m_eraCount) + " resumes on the relabeled window)"));
|
|
//--- DERIVE THE GEOMETRY FROM WHAT WAS JUST MEASURED, then relabel under it. Only at era 0: changing
|
|
//--- the barrier mid-run would move the target out from under weights already fitted to the old one.
|
|
//--- Iterated because the horizon scales with the target and the excursions are measured over the
|
|
//--- horizon (see BARRIER_DERIVE_MAX_PASSES) - one pass would size the target from travel measured
|
|
//--- under the previous horizon.
|
|
if(m_eraCount == 0 && m_geometryDerivePasses < BARRIER_DERIVE_MAX_PASSES)
|
|
{
|
|
double prevSl = m_derivedSlMult, prevTp = m_derivedTpMult;
|
|
m_geometryDerivePasses++;
|
|
if(DeriveBarrierGeometry())
|
|
{
|
|
bool settled = (prevSl > 0.0 && prevTp > 0.0
|
|
&& MathAbs(m_derivedSlMult - prevSl) <= BARRIER_DERIVE_TOLERANCE * prevSl
|
|
&& MathAbs(m_derivedTpMult - prevTp) <= BARRIER_DERIVE_TOLERANCE * prevTp);
|
|
if(!settled)
|
|
{
|
|
if(m_geometryDerivePasses >= BARRIER_DERIVE_MAX_PASSES)
|
|
Print(ID + StringFormat(": barrier geometry did NOT settle within %d passes (last move "
|
|
"%.2f->%.2f stop, %.2f->%.2f target). Using the latest pair; the "
|
|
"reachability figures above are the ones to check.",
|
|
BARRIER_DERIVE_MAX_PASSES, prevSl, m_derivedSlMult, prevTp,
|
|
m_derivedTpMult));
|
|
else
|
|
{
|
|
//--- Re-derive the horizon for the NEW target and relabel the whole window under it.
|
|
//--- Train()'s !m_labelCachePrebuilt gate restarts the scan on the next call.
|
|
m_barrierHorizonResolved = false;
|
|
m_labelCachePrebuilt = false;
|
|
ArrayInitialize(m_labelCacheHasValue, false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//--- Cold-start fix: a freshly-initialized (random-weight) network's argmax is close to uniform
|
|
//--- noise across the 3 classes, so on this typically heavily-skewed label distribution it fires
|
|
//--- far more non-majority-class calls at the very start of era 0 than the true base rate warrants,
|
|
//--- until enough backProp steps correct it. Now that the real prior is known, push the output
|
|
//--- layer's bias toward whichever class actually dominates - only the bias term moves, the
|
|
//--- per-input weights stay randomly initialized and still carry the real learning signal. Only
|
|
//--- meaningful for the 3-output classification head, and only for a fresh net (this whole prebuild
|
|
//--- path is skipped entirely when a trained net was loaded from disk - see m_labelCachePrebuilt).
|
|
//--- m_eraCount==0 gate: the prebuild can also re-run MID-run now (new-bar cache invalidation -
|
|
//--- see Train()'s era-start wipe check); stomping a partially-trained net's output biases with
|
|
//--- +-3.0 cold-start values there would erase real learned calibration, so fresh runs only.
|
|
if(m_outputNeuronsCount == 3 && m_eraCount == 0)
|
|
{
|
|
int dominant = 2; // Neutral
|
|
int dominantCount = m_labelPrebuildNeutralCount;
|
|
if(m_labelPrebuildBuyCount > dominantCount)
|
|
{
|
|
dominant = 0;
|
|
dominantCount = m_labelPrebuildBuyCount;
|
|
}
|
|
if(m_labelPrebuildSellCount > dominantCount)
|
|
{
|
|
dominant = 1;
|
|
dominantCount = m_labelPrebuildSellCount;
|
|
}
|
|
int totalLabeled = m_labelPrebuildBuyCount + m_labelPrebuildSellCount + m_labelPrebuildNeutralCount;
|
|
//--- Trigger raised 0.40 -> COLD_START_SEED_MIN_DOMINANCE with the triple-barrier relabel. This seed
|
|
//--- is an antidote to an EXTREME prior: under the old exact-pivot target Neutral held ~94% of bars
|
|
//--- and a uniform-ish random argmax over-called wildly for the first few thousand steps. Barrier
|
|
//--- labels land near 25/25/50, where sigmoid(+-3) ~ 0.95/0.05 is no longer a correction but a
|
|
//--- distortion - it would start the net further from the truth than random init does. Keeping the
|
|
//--- mechanism behind a genuinely-dominant threshold means it stays available for a skewed symbol
|
|
//--- (or a tight-target configuration that pushes Neutral back up) and self-disables otherwise.
|
|
if(totalLabeled > 0 && (double)dominantCount / totalLabeled > COLD_START_SEED_MIN_DOMINANCE)
|
|
{
|
|
const double BIAS_MAGNITUDE = 3.0; // sigmoid(+-3) ~= 0.95/0.05 - comfortably outweighs a
|
|
// fresh network's random per-input weighted-sum noise
|
|
double biasValues[3] = { -BIAS_MAGNITUDE, -BIAS_MAGNITUDE, -BIAS_MAGNITUDE };
|
|
biasValues[dominant] = BIAS_MAGNITUDE;
|
|
if(Net.SeedOutputLayerBias(biasValues))
|
|
PrintVerbose(ID + ": seeded output layer bias toward " + EnumToString((ENUM_SIGNAL)(dominant == 0 ? Buy : dominant == 1 ? Sell : Neutral)) +
|
|
" (era 0 cold-start fix)");
|
|
}
|
|
}
|
|
}
|
|
//--- ConfirmedZigZagLabel() REMOVED 2026-08-01. It was the online-learning path's copy of the exact-pivot
|
|
//--- target; that target is gone, and its one caller now asks TripleBarrierLabel() the same question
|
|
//--- training asks. Keeping a second label rule alive is how the live and trained tasks drift apart.
|
|
#endif // WARRIOR_AIBASE_LABELS_MQH
|