forked from animatedread/Warrior_EA
Corrects the premise of the previous plan. Break-even is NOT a ceiling. If the model shifts the win probability on the bars it selects from p0 = m/(m+k) to p0 + d, then EV = (p0+d)*k - (1-p0-d)*m = d*(k+m) because p0*k - (1-p0)*m is zero by construction. The stop:target RATIO is expectancy-neutral - a punishing break-even is exactly repaid by the payoff - and only the real edge d and the TOTAL WIDTH (k+m) move EV. Width matters because the spread is charged once per trade however wide the barriers are, so a narrow barrier spends much of its own range on costs. DeriveBarrierGeometry's own comment already said the ratio buys nothing; the objective just never followed from it. Blocker this had to solve first: m_excUpCache/m_excDownCache hold only MAXIMUM travel each way, and a maximum cannot say which side was reached FIRST - so any geometry other than the walked one was undecidable on precisely the bars where both barriers were touched, ~28% of the sample. - BARRIER_LADDER: per bar, the first-touch AGE for 8 travel distances in each direction, filled during the walk the labels already run. Cursors keep it O(1) amortised per walked bar rather than 16 comparisons. Levels are travel FROM ENTRY, not barrier prices, so one ladder serves both directions and the spread is applied analytically when a level converts back to an SL/TP multiple - storing prices would need four ladders and bake today's spread into the cache. Sized, invalidated and validity-gated with the label caches. - ReportGeometryExpectancyScan: every ladder pair priced exactly off that cache - width in ATR and in SPREADS (cost efficiency, knowable without knowing d), break-even, both base rates, the share of bars resolved inside the horizon, and EV per unit of edge. Compares the widest resolvable pair against the quantile rule's pick. MEASUREMENT ONLY - the quantile rule still chooses. Nothing here can measure d, and width buys nothing if the wider target is less predictable. Base rates are printed beside each break-even because a persistent gap is DRIFT and must not be credited to the model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1105 lines
67 KiB
MQL5
1105 lines
67 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);
|
|
//--- Same lifetime and the same validity flag as the excursion caches beside them - see BARRIER_LADDER.
|
|
ArrayResize(m_ladderUpAt, bars * BARRIER_LADDER_COUNT);
|
|
ArrayResize(m_ladderDownAt, bars * BARRIER_LADDER_COUNT);
|
|
ArrayInitialize(m_ladderUpAt, 0);
|
|
ArrayInitialize(m_ladderDownAt, 0);
|
|
ArrayResize(m_winLongCache, bars);
|
|
ArrayResize(m_winShortCache, 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. |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| Expectancy of every ladder pair, measured exactly off the |
|
|
//| first-passage cache (see BARRIER_LADDER). |
|
|
//| |
|
|
//| WHY THIS OBJECTIVE. Let the model shift the win probability on the |
|
|
//| bars it selects from the base rate p0 = m/(m+k) to p0 + d. Then |
|
|
//| EV = (p0+d)*k - (1-p0-d)*m = d*(k+m), |
|
|
//| because p0*k - (1-p0)*m is zero by construction. So the stop:target|
|
|
//| RATIO is expectancy-neutral - a punishing break-even is exactly |
|
|
//| repaid by the payoff - and only two things move EV: the real edge |
|
|
//| d, and the TOTAL BARRIER WIDTH (k+m). Width matters because the |
|
|
//| spread is charged once per trade however wide the barriers are, so |
|
|
//| a narrow barrier spends a large share of its own range on costs. |
|
|
//| That is why every row below reports width in SPREADS as well as in |
|
|
//| ATR: it is the cost efficiency of the geometry, and it is knowable |
|
|
//| without knowing d. |
|
|
//| |
|
|
//| WHAT IT DOES NOT DO: measure d. Nothing here can - d is a property |
|
|
//| of the model and the features, not of the barrier - so this cannot |
|
|
//| and must not be read as "this geometry is profitable". It answers |
|
|
//| the narrower question the previous rule never asked: GIVEN an edge,|
|
|
//| which geometry converts the most of it into money, and what does |
|
|
//| each pair cost in spread and in trade frequency. |
|
|
//| |
|
|
//| The base rates are printed beside each break-even deliberately. On |
|
|
//| a driftless walk they coincide; a persistent gap is DRIFT (being |
|
|
//| long pays on an index) and must never be credited to the model - |
|
|
//| see chancePrecPct, which is measured for exactly that reason. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::ReportGeometryExpectancyScan(void)
|
|
{
|
|
int bars = m_labelCacheBars;
|
|
if(bars <= 0 || ArraySize(m_ladderUpAt) < bars * BARRIER_LADDER_COUNT)
|
|
return;
|
|
//--- IS region only, matching DeriveBarrierGeometry and 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));
|
|
int from = MathMax(oosCutoff, 0);
|
|
double spread = (double)m_symbol.Spread() * m_symbol.Point();
|
|
if(!MathIsValidNumber(spread) || spread < 0.0)
|
|
spread = 0.0;
|
|
//--- Spread expressed in ATR, averaged over the same bars the ladder covers - the ladder is in ATR
|
|
//--- units, so the cost has to be converted into the same units before it can be netted off a leg.
|
|
double spreadAtrSum = 0.0;
|
|
int atrN = 0;
|
|
for(int i = from; i < bars; i++)
|
|
{
|
|
if(i >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[i])
|
|
continue;
|
|
double a = m_ATR.Main(i);
|
|
if(!MathIsValidNumber(a) || a <= 0.0)
|
|
continue;
|
|
spreadAtrSum += spread / a;
|
|
atrN++;
|
|
}
|
|
if(atrN < BARRIER_DERIVE_MIN_SAMPLES)
|
|
return;
|
|
double spreadAtr = spreadAtrSum / atrN;
|
|
Print(ID + StringFormat(": barrier expectancy scan - spread averages %.3f*ATR over %d bars. EV per "
|
|
"trade = edge x width, so the ratio is EV-neutral and WIDTH is what pays; "
|
|
"'spreads' is width/spread (cost efficiency), 'decided' is the share of bars "
|
|
"the long side resolved inside the %d-bar horizon. No row here demonstrates "
|
|
"an edge - it prices one.", spreadAtr, atrN, m_barrierHorizonBars));
|
|
double bestWidth = -1.0;
|
|
int bestT = -1, bestS = -1;
|
|
for(int tL = 0; tL < BARRIER_LADDER_COUNT; tL++)
|
|
for(int sL = 0; sL < BARRIER_LADDER_COUNT; sL++)
|
|
{
|
|
//--- Ladder levels are TRAVEL from the entry close; converting back to the SL/TP multiples that
|
|
//--- would actually be pinned puts the spread where the fill puts it - see BARRIER_LADDER.
|
|
double reward = BARRIER_LADDER[tL] - spreadAtr;
|
|
double risk = BARRIER_LADDER[sL] + spreadAtr;
|
|
if(reward <= 0.0 || risk <= 0.0)
|
|
continue; // target inside the spread - not a tradeable geometry at any hit rate
|
|
long nLong = 0, nShort = 0, nDecided = 0, nSeen = 0;
|
|
for(int i = from; i < bars; i++)
|
|
{
|
|
if(i >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[i])
|
|
continue;
|
|
int b = i * BARRIER_LADDER_COUNT;
|
|
int tUpT = m_ladderUpAt[b + tL]; // long target / short stop reference
|
|
int tDnS = m_ladderDownAt[b + sL]; // long stop
|
|
int tDnT = m_ladderDownAt[b + tL]; // short target
|
|
int tUpS = m_ladderUpAt[b + sL]; // short stop
|
|
//--- 0 means "never touched inside the horizon". A smaller age is the EARLIER touch, and a
|
|
//--- tie goes to the stop - the same pessimistic convention the label walk uses, so these
|
|
//--- numbers describe the same game the training target does.
|
|
if(tUpT > 0 && (tDnS == 0 || tUpT < tDnS))
|
|
nLong++;
|
|
if(tDnT > 0 && (tUpS == 0 || tDnT < tUpS))
|
|
nShort++;
|
|
if(tUpT > 0 || tDnS > 0)
|
|
nDecided++;
|
|
nSeen++;
|
|
}
|
|
if(nSeen < BARRIER_DERIVE_MIN_SAMPLES)
|
|
continue;
|
|
double pL = 100.0 * nLong / nSeen;
|
|
double pS = 100.0 * nShort / nSeen;
|
|
double be = 100.0 * risk / (risk + reward);
|
|
double width = risk + reward;
|
|
double decided = 100.0 * nDecided / nSeen;
|
|
PrintFormat("%s: stop %.2f target %.2f | width %.2f*ATR = %.1f spreads | break-even %.1f%% |"
|
|
" base long %.1f%% short %.1f%% | decided %.1f%% | EV at a 1pp edge %.4f*ATR",
|
|
ID, risk, reward, width, (spreadAtr > 0.0 ? width / spreadAtr : 0.0), be,
|
|
pL, pS, decided, 0.01 * width);
|
|
//--- The recommendation is the WIDEST pair that still resolves most of its bars inside the
|
|
//--- horizon. Width is the whole of the EV multiplier; the decided-rate floor is what stops it
|
|
//--- running away to a barrier the horizon can never deliver, which is the failure the shipped
|
|
//--- 128-bar clamp already caused once.
|
|
if(decided >= 60.0 && width > bestWidth)
|
|
{
|
|
bestWidth = width;
|
|
bestT = tL;
|
|
bestS = sL;
|
|
}
|
|
}
|
|
if(bestT >= 0)
|
|
Print(ID + StringFormat(": barrier expectancy scan - on width alone the best resolvable pair is "
|
|
"stop %.2f*ATR target %.2f*ATR (width %.2f*ATR, %.1f spreads), against the "
|
|
"quantile rule's stop %.2f target %.2f (width %.2f*ATR, %.1f spreads) - a "
|
|
"%.2fx difference in EV per unit of edge. MEASUREMENT ONLY: the quantile "
|
|
"rule still chooses, because width buys nothing if the wider target is "
|
|
"less predictable, and this scan cannot see that.",
|
|
BARRIER_LADDER[bestS] + spreadAtr, BARRIER_LADDER[bestT] - spreadAtr,
|
|
bestWidth, (spreadAtr > 0.0 ? bestWidth / spreadAtr : 0.0),
|
|
m_derivedSlMult, m_derivedTpMult, m_derivedSlMult + m_derivedTpMult,
|
|
(spreadAtr > 0.0 ? (m_derivedSlMult + m_derivedTpMult) / spreadAtr : 0.0),
|
|
(m_derivedSlMult + m_derivedTpMult > 0.0
|
|
? bestWidth / (m_derivedSlMult + m_derivedTpMult) : 0.0)));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
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, so the stop used for labelling is the stop that can actually be
|
|
//--- placed. This is the ONLY adjustment either leg receives - both multiples are otherwise read
|
|
//--- straight off the measured distributions.
|
|
if(slRaw < MIN_SL_ATR_MULTIPLIER)
|
|
slRaw = MIN_SL_ATR_MULTIPLIER;
|
|
//--- The minimum-reward:risk raise that used to sit here is GONE (2026-08-09). It overrode a measured
|
|
//--- target with an arithmetic one - twice whatever the stop happened to be - and on SP500 H1 that
|
|
//--- pushed the target from the q50 of favourable travel out to 6.66*ATR, reachable on 3.3% of bars.
|
|
//--- The model was then trained to predict an outcome that essentially never happens. A measured
|
|
//--- target has to stay measured; see Variables\Inputs.mqh for why the ratio bought nothing in
|
|
//--- exchange (a reward:risk floor moves payoff and hit rate together at a fixed break-even, it does
|
|
//--- not create expectancy) and cost two separate outages.
|
|
//--- 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;
|
|
//--- Publish to the LIVE order path (ConfidenceBridge.mqh). Until 2026-08-09 the derived pair
|
|
//--- reached the labels only, so the gate certified trades at this geometry while OpenParams()
|
|
//--- placed them at the enum geometry - graded on one game, paid on another.
|
|
g_DerivedSlAtrMult = m_derivedSlMult;
|
|
g_DerivedTpAtrMult = m_derivedTpMult;
|
|
Print(ID + StringFormat(": live orders now use the MEASURED geometry - stop %.2f*ATR, target "
|
|
"%.2f*ATR - overriding the SL_Mode/TP_Mode enums (and the Intelligent "
|
|
"modes' confidence scaling), so the trade placed is the trade the deploy "
|
|
"gate certified.", m_derivedSlMult, m_derivedTpMult));
|
|
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) | 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,
|
|
tpReach, slReach, breakeven));
|
|
//--- Prices every alternative geometry against the one just chosen. Runs AFTER the pick so the report
|
|
//--- can compare the two, and changes nothing - see its definition for why width, not ratio, is the
|
|
//--- quantity that moves expectancy.
|
|
ReportGeometryExpectancyScan();
|
|
//--- Reachability is still worth warning about, but the diagnosis has changed. Nothing inflates the
|
|
//--- target any more, so a target the market rarely reaches can only mean the HORIZON is too short
|
|
//--- for the favourable travel this instrument actually produces - the q50 of MFE should by
|
|
//--- construction be reached about half the time, and a reading far under that says the excursion
|
|
//--- window is cutting the measurement off. That is a horizon problem, not a ratio problem, and it
|
|
//--- is the same failure the clamped-horizon incident produced.
|
|
if(tpReach < BARRIER_MIN_TP_REACH_PCT)
|
|
Print(ID + StringFormat(": WARNING - the MEASURED target of %.2f*ATR is reached on only %.1f%% of "
|
|
"bars inside the %d-bar horizon. It was taken from the q%.0f of favourable "
|
|
"travel, so it should be reached on roughly %.0f%% - a reading this far "
|
|
"below means the horizon is truncating the excursions the geometry is "
|
|
"derived from, and the labels inherit that truncation.",
|
|
m_derivedTpMult, tpReach, m_barrierHorizonBars,
|
|
100.0 * BARRIER_TP_QUANTILE, 100.0 * (1.0 - BARRIER_TP_QUANTILE)));
|
|
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;
|
|
//--- MOVED UP from the bottom of the walk (2026-08-09) for exactly the reason written above about the
|
|
//--- excursions: the two early returns below this line return WITHOUT reaching the assignment that
|
|
//--- used to be the only one, so an unresolvable bar published the PREVIOUS bar's timeout verdict. The
|
|
//--- both-won flags are new and are cleared here from the start rather than inheriting that bug.
|
|
m_lastBarrierTimedOut = false;
|
|
m_lastBarrierBothWon = false;
|
|
m_lastBarrierBothWonTied = false;
|
|
m_lastWinLong = false;
|
|
m_lastWinShort = false;
|
|
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;
|
|
//--- Bar index at which each target was FIRST reached, for the both-won resolution below. The walk
|
|
//--- runs t = idx-1 downward, i.e. forward in time, so the LARGER t is the earlier touch.
|
|
int longWonAt = -1, shortWonAt = -1;
|
|
//--- 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
|
|
//--- First-passage ladder for THIS bar (see BARRIER_LADDER). Cursors, not a full rescan: the ladder is
|
|
//--- ascending and travel is monotone in the running extreme, so once a level is passed no lower level
|
|
//--- can be reached later - each level is tested until it trips exactly once, which keeps this O(1)
|
|
//--- amortised per walked bar instead of 2 x BARRIER_LADDER_COUNT comparisons on every one.
|
|
ArrayInitialize(m_lastLadderUpAt, 0);
|
|
ArrayInitialize(m_lastLadderDownAt, 0);
|
|
int upCursor = 0, dnCursor = 0;
|
|
//--- 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;
|
|
}
|
|
//--- First-passage ladder. Runs over the WHOLE horizon, not excWindow: this measures how a trade
|
|
//--- held to its barriers would have resolved, so it must see every bar the trade would have been
|
|
//--- open for. Travel is measured from `entry` (the close) with no spread applied - see
|
|
//--- BARRIER_LADDER for why, and for how a level converts back into an SL/TP multiple.
|
|
int age = idx - t;
|
|
while(upCursor < BARRIER_LADDER_COUNT && hi >= entry + BARRIER_LADDER[upCursor] * atr)
|
|
{
|
|
m_lastLadderUpAt[upCursor] = age;
|
|
upCursor++;
|
|
}
|
|
while(dnCursor < BARRIER_LADDER_COUNT && lo <= entry - BARRIER_LADDER[dnCursor] * atr)
|
|
{
|
|
m_lastLadderDownAt[dnCursor] = age;
|
|
dnCursor++;
|
|
}
|
|
//--- 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;
|
|
longWonAt = t;
|
|
}
|
|
}
|
|
if(!shortWon && !shortLost)
|
|
{
|
|
if(hi >= shortSl)
|
|
shortLost = true;
|
|
else
|
|
if(lo <= shortTp)
|
|
{
|
|
shortWon = true;
|
|
shortWonAt = t;
|
|
}
|
|
}
|
|
//--- 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);
|
|
}
|
|
//--- Published BEFORE the collapse to a single label, because the collapse cannot be undone afterwards
|
|
//--- and these are what profitability is actually a function of. Every return below this point carries
|
|
//--- them; the early returns above leave them false, which is correct - an unresolvable bar has no
|
|
//--- winning direction.
|
|
m_lastWinLong = longWon;
|
|
m_lastWinShort = shortWon;
|
|
if(longWon && !shortWon)
|
|
return Buy;
|
|
if(shortWon && !longWon)
|
|
return Sell;
|
|
//--- BOTH TARGETS REACHED. This comment used to say the branch was "unreachable" for every shipped SL/TP
|
|
//--- pairing, and while reward >= risk that was true - reaching one side's stop necessarily crossed the
|
|
//--- other side's nearer target first, so the two outcomes were complementary. Removing the
|
|
//--- minimum-reward:risk raise (2026-08-09) ended that: the MEASURED geometry puts the target at the q50
|
|
//--- of favourable travel and the stop at the q75 of adverse, i.e. target CLOSER than stop, and price
|
|
//--- that swings +target then -target inside one horizon wins in BOTH directions.
|
|
//---
|
|
//--- Falling through to Neutral here was actively harmful, and not marginally: on SP500 H1 it labelled
|
|
//--- ~27% of all bars "do not trade" when a trade in EITHER direction would have collected its target.
|
|
//--- Those are the cleanest positives in the sample, and they were being handed to the model as the
|
|
//--- abstain class - while the confidence threshold downstream was being asked to find selectivity in
|
|
//--- what was left.
|
|
//---
|
|
//--- Resolved by FIRST TOUCH: the target reached earlier is the trade that would have closed first, and
|
|
//--- it is the direction the bar actually moved in before it reversed. Deterministic, and no more
|
|
//--- lookahead than any other part of this walk - it reads the same forward window.
|
|
if(longWon && shortWon)
|
|
{
|
|
m_lastBarrierBothWon = true;
|
|
if(longWonAt > shortWonAt) // larger t = earlier bar, see the declaration
|
|
return Buy;
|
|
if(shortWonAt > longWonAt)
|
|
return Sell;
|
|
//--- Same bar. OHLC carries no intrabar ordering, and the whole file's convention is to refuse the
|
|
//--- ordering it cannot see rather than guess it (BARRIER_TIE_GOES_TO_STOP). Unlike the stop tie
|
|
//--- there is no pessimistic side to fall to - both directions won - so the bar stays Neutral and
|
|
//--- is counted, because a guess here would inject a coin-flip direction into the training target.
|
|
//--- Requires a single bar spanning both targets, ~2x the measured target in range, so it should be
|
|
//--- rare; m_labelPrebuildBothWonTieCount is what confirms that rather than assuming it.
|
|
m_lastBarrierBothWonTied = true;
|
|
return Neutral;
|
|
}
|
|
//--- 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 unorderable both-won tie - nothing tradeable here
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
//--- Published so EnsureBarrierHorizon can refuse to LATCH a fallback: right after a terminal
|
|
//--- restart the ZigZag handle has calculated nothing yet, and a horizon computed from 0 legs is
|
|
//--- the indicator's warm-up state, not a property of the instrument.
|
|
m_barrierHorizonLegStarved = (count < BARRIER_HORIZON_MIN_SAMPLES);
|
|
if(!m_barrierHorizonLegStarved)
|
|
{
|
|
ArraySort(gaps);
|
|
swingMedian = gaps[count / 2];
|
|
}
|
|
else
|
|
if(!m_horizonStarvedWarned)
|
|
{
|
|
m_horizonStarvedWarned = true;
|
|
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 PROVISIONALLY - re-resolved on the "
|
|
"next label-cache rebuild, once the indicator has caught up");
|
|
}
|
|
//--- 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;
|
|
int prevHorizon = m_barrierHorizonBars;
|
|
m_barrierHorizonBars = ComputeBarrierHorizonBars(bars);
|
|
//--- A leg-starved computation is the fallback, not a measurement - keep it PROVISIONAL so the next
|
|
//--- full rebuild recomputes it, instead of latching an indicator warm-up artifact for the process
|
|
//--- lifetime (see m_barrierHorizonLegStarved).
|
|
m_barrierHorizonResolved = !m_barrierHorizonLegStarved;
|
|
//--- If a re-resolution actually MOVED the horizon, any label cached under the old one answers a
|
|
//--- different question - wipe, and let the prebuild refill under one rule. The wipe is cheap
|
|
//--- relative to training on labels from two different horizons, which is the exact failure the
|
|
//--- once-per-process latch exists to prevent.
|
|
if(m_barrierHorizonBars != prevHorizon && ArraySize(m_labelCacheHasValue) > 0)
|
|
{
|
|
ArrayInitialize(m_labelCacheHasValue, false);
|
|
m_labelCachePrebuilt = false;
|
|
}
|
|
double slMultLog, tpMultLog;
|
|
BarrierMultiples(slMultLog, tpMultLog);
|
|
//--- PROVISIONAL vs FINAL. The geometry can only be derived from measured excursions, and excursions only
|
|
//--- exist once bars have been labelled, so the first pass necessarily labels with the enum fallback and
|
|
//--- prints it here. It is then discarded: DeriveBarrierGeometry() runs, the horizon re-resolves, the
|
|
//--- cache is wiped and this line prints again with the measured pair. Reading the log without knowing
|
|
//--- that, the first line looks exactly like a config change that failed to take effect - which is how it
|
|
//--- was in fact read. Say which one this is.
|
|
string stage = m_geometryDerived
|
|
? " | MEASURED geometry, this is what trains"
|
|
: " | PROVISIONAL - enum fallback for the measurement pass only, superseded by the "
|
|
"DERIVED pair logged next";
|
|
if(m_barrierHorizonLegStarved)
|
|
stage += " | horizon PROVISIONAL (ZigZag still warming up, re-resolved on the next rebuild)";
|
|
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" + stage);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
//--- IS-ONLY, matching the final tally pass exactly. These counters are reported as percentages OF the
|
|
//--- Buy/Sell/Neutral tallies, and those are IS-only - the timeout count was previously incremented over
|
|
//--- the whole scan and then divided by an in-sample denominator, so its "% of Neutral" could read high
|
|
//--- for no reason but the split. A diagnostic that mixes two populations is worse than no diagnostic:
|
|
//--- it is the horizon check, and it has to be trustworthy to do its job.
|
|
bool countable = (idx >= MathMax(2, m_labelPrebuildOosCutoff)
|
|
&& idx <= bars - MathMax(m_historyBars, 0) - 1);
|
|
if(countable)
|
|
{
|
|
if(verdict == Neutral && m_lastBarrierTimedOut)
|
|
m_labelPrebuildTimeoutCount++;
|
|
//--- Counted for EVERY verdict, not just Neutral: after first-touch resolution most both-won bars
|
|
//--- now carry a direction, and the interesting number is how much of the label set this class is -
|
|
//--- not how much of it stayed unresolved.
|
|
if(m_lastBarrierBothWon)
|
|
{
|
|
m_labelPrebuildBothWonCount++;
|
|
if(m_lastBarrierBothWonTied)
|
|
m_labelPrebuildBothWonTieCount++;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
//--- Published under the SAME validity flag as the label and the excursions, for the same reason:
|
|
//--- a reader must never see one without the others (see BARRIER_LADDER).
|
|
int ladderBase = idx * BARRIER_LADDER_COUNT;
|
|
if(ladderBase + BARRIER_LADDER_COUNT <= ArraySize(m_ladderUpAt))
|
|
for(int L = 0; L < BARRIER_LADDER_COUNT; L++)
|
|
{
|
|
m_ladderUpAt[ladderBase + L] = m_lastLadderUpAt[L];
|
|
m_ladderDownAt[ladderBase + L] = m_lastLadderDownAt[L];
|
|
}
|
|
if(idx < ArraySize(m_winLongCache))
|
|
{
|
|
m_winLongCache[idx] = m_lastWinLong;
|
|
m_winShortCache[idx] = m_lastWinShort;
|
|
}
|
|
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)
|
|
{
|
|
//--- Not armed until the history is synced - the caller retries on its next scheduled call. Without
|
|
//--- this, a terminal restart ran the resumed-model pre-scan in the same second as OnInit, against
|
|
//--- whatever the terminal had loaded so far.
|
|
if(!SeriesInfoInteger(m_symbol.Name(), PERIOD_CURRENT, SERIES_SYNCHRONIZED))
|
|
return;
|
|
//--- A model that is still TRAINING sizes its window by the training rule, not by the saved study
|
|
//--- watermark. The watermark of a caught-up model sits at its last studied bar, so on resume
|
|
//--- Bars(dtStudied, now) is ~0 and the whole pipeline downstream ran on an empty window: a zero-bar
|
|
//--- "complete" cache ("Buy: 0 | Sell: 0 | Neutral: 0"), a horizon from 0 ZigZag legs, no geometry.
|
|
//--- Train()'s own era start applies this exact reset (TrainWindowStart) - this makes the pre-scan
|
|
//--- and the era loop agree. Deployed (complete) models keep their watermark: for them dtStudied
|
|
//--- gates INFERENCE recency, and this scan must not touch it.
|
|
if(!m_trainingComplete)
|
|
dtStudied = TrainWindowStart(m_tuneStartTrainBar);
|
|
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_labelPrebuildBothWonCount = 0;
|
|
m_labelPrebuildBothWonTieCount = 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;
|
|
//--- BOTH-WON composition. Reported unconditionally rather than only when non-zero, because zero is
|
|
//--- itself the answer to "is the target closer than the stop" and a line that vanishes cannot say so.
|
|
//--- The tie share is the one to watch: it is the only part of this class still landing in Neutral, and
|
|
//--- if it is not small then first-touch resolution is not actually recovering these bars.
|
|
string prebuildBothWon = (prebuildTotal > 0)
|
|
? " | both targets reached (target nearer than stop) " + IntegerToString(m_labelPrebuildBothWonCount) +
|
|
" = " + DoubleToString(100.0 * m_labelPrebuildBothWonCount / prebuildTotal, 1) +
|
|
"% of bars, resolved by first touch; " + IntegerToString(m_labelPrebuildBothWonTieCount) +
|
|
" same-bar tie" + (m_labelPrebuildBothWonTieCount == 1 ? "" : "s") + " left Neutral"
|
|
: "";
|
|
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 + prebuildBothWon +
|
|
" | 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. At era 0, OR whenever
|
|
//--- no pair is pinned yet (m_geometryDerived false): the "only at era 0" form of this gate meant a
|
|
//--- RESUMED model whose .cfg carried no derived pair could never derive - it fell back to the enum
|
|
//--- barriers permanently, relabelling weights that had been trained on the measured pair. The
|
|
//--- mid-run stability the era gate was protecting is carried by m_geometryDerived itself: once a
|
|
//--- pair is derived or adopted it is never re-derived, so a mid-run rebuild still cannot move the
|
|
//--- target under a fitted model.
|
|
//--- NOT while the horizon is leg-starved: the excursion window would be the indicator's warm-up
|
|
//--- fallback, and a pair derived from it gets PINNED (in the .cfg, below) - pinning an artifact.
|
|
//--- 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_geometryDerived) && !m_barrierHorizonLegStarved &&
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//--- PIN THE SETTLED PAIR TO DISK. The .cfg was only ever written at model creation and at
|
|
//--- weights-reset - both BEFORE era 0's derivation - so the derived pair lived exclusively in
|
|
//--- memory: every restart read back zeros, adopted nothing, fell back to the enum barriers, and
|
|
//--- (with the era gate above) could never re-derive. A full day of training on the measured
|
|
//--- 3.33/1.62 pair resumed as 2:6 the moment the terminal restarted. One-shot per process; the
|
|
//--- adoption path sets the flag too, since there the pair is already on disk.
|
|
if(m_geometryDerived && !m_geometryCfgSaved)
|
|
{
|
|
m_geometryCfgSaved = true;
|
|
if(SaveTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount,
|
|
m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo,
|
|
m_historyBars, m_outputNeuronsCount, m_neuronsCount,
|
|
LEGACY_STUDY_PERIOD_SLOT, m_minTrainYear, m_isInitialized,
|
|
LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount,
|
|
m_lstmHiddenSize, m_activeFileCommon))
|
|
Print(ID + StringFormat(": derived geometry PINNED to the .cfg - stop %.2f*ATR, target "
|
|
"%.2f*ATR. A restart now adopts this pair instead of falling back "
|
|
"to the enum barriers.", m_derivedSlMult, m_derivedTpMult));
|
|
else
|
|
Print(ID + ": WARNING - failed to pin the derived geometry to the .cfg; a restart will "
|
|
"re-derive it from the same data instead of adopting it.");
|
|
}
|
|
//--- 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
|