//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //| Triple-barrier labelling and the async label-cache prebuild. | //+------------------------------------------------------------------+ #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 - the ladder //--- sizes its own three arrays together so they can never disagree about how many bars they cover. m_ladder.Allocate(bars); ArrayResize(m_winLongCache, bars); ArrayResize(m_winShortCache, bars); //--- Sized with the caches and zeroed, so a bar the scan has not reached yet reads as "no opinion" //--- (0.0 = abstain) rather than as last era's decision - which would let a stale vote close a trade //--- in the simulation that nothing would have closed live. ArrayResize(m_oosDecisionSeries, bars); ArrayInitialize(m_oosDecisionSeries, 0.0); 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. | //+------------------------------------------------------------------+ 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). | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ReportGeometryExpectancyScan(void) { int bars = m_labelCacheBars; if(bars <= 0 || !m_ladder.Has(bars - 1)) 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; //--- Published so CostAdjustedBreakEvenPct() can price the cost into every break-even the run quotes. m_spreadAtr = spreadAtr; 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++) { //--- Every ladder pairing walks the whole labelled window again. Purely a pricing report - it //--- adopts nothing - so leaving with the rows printed so far costs only information. if(ShutdownRequested()) return; 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; //--- Both sides of the SAME pair, resolved by the ladder's own first-touch rule: 0 means //--- "never touched inside the horizon", a smaller age is the EARLIER touch, and a tie //--- goes to the stop. That convention lives in FirstTouch so these numbers describe the //--- same game the training target does - by construction, not by matching comments. if(m_ladder.FirstTouch(i, true, sL, tL) > 0) nLong++; if(m_ladder.FirstTouch(i, false, sL, tL) > 0) nShort++; if(m_ladder.UpAge(i, tL) > 0 || m_ladder.DownAge(i, sL) > 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; //--- CONDITIONAL source for the fractal target: quantiles of the leg-scoped MFE/MAE recorded at //--- Buy/Sell-LABELED bars (see m_fracLegFav) instead of the pooled every-bar excursions. bool labUp[]; int idxList[]; ArrayResize(labUp, 0); ArrayResize(idxList, 0); bool conditional = false; if(IsFractalTarget() && m_fracLegCount >= BARRIER_DERIVE_MIN_SAMPLES) { conditional = true; n = m_fracLegCount; ArrayResize(up, n); ArrayResize(dn, n); for(int i = 0; i < n; i++) { up[i] = m_fracLegFav[i]; dn[i] = m_fracLegAdv[i]; } } else { if(IsFractalTarget()) Print(ID + StringFormat(": conditional geometry NOT available - only %d labeled fractal legs " "(need %d); deriving from the pooled every-bar excursions instead.", m_fracLegCount, BARRIER_DERIVE_MIN_SAMPLES)); //--- 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; //--- CONSISTENCY CHECK + first-passage reachability, harvested on the SAME bar in the SAME pass. ArrayResize(labUp, n + 1); labUp[n] = (i < ArraySize(m_labelCacheBuy) && m_labelCacheBuy[i]); ArrayResize(idxList, n + 1); idxList[n] = i; 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); //--- THE STOP LADDER, read in one call. MathQuantile sorts its own copy, so up[]/dn[] keep the bar //--- order their labels are paired by - which is what the consistency report below needs, and what //--- an explicit unsorted copy used to be kept for. up[] was being sorted for no consumer at all. double slLadder[]; if(!MathQuantile(dn, BARRIER_SL_QUANTILE_LADDER, slLadder)) { Print(ID + StringFormat(": barrier geometry NOT derived - the stop ladder could not be read off" " %d adverse-excursion samples. Falling back to the configured %d:%d.", n, m_sl_mode, m_tp_mode)); return false; } //--- STOP from the ADVERSE distribution, TARGET from the FAVOURABLE one - each leg sized by the //--- thing it actually has to survive or reach. Every rung is reported so the choice is //--- auditable. double slRaw = 0.0, tpRaw = 0.0; double chosenQ = 0.0, chosenReach = 0.0, chosenRr = BARRIER_TARGET_RR_MIN; string rungRows = ""; for(int r = 0; r < BARRIER_SL_QUANTILE_COUNT; r++) { double q = BARRIER_SL_QUANTILE_LADDER[r]; double sl = slLadder[r]; if(sl < MIN_SL_ATR_MULTIPLIER) sl = MIN_SL_ATR_MULTIPLIER; //--- RATIO = the policy FLOOR, raised toward what the swings actually offer - and the raise //--- has to be EARNED against measured reachability, one rung at a time. double rrWant = (m_swingMedianLegAtr > 0.0) ? BarrierSnapRr(m_swingMedianLegAtr / sl) : BARRIER_TARGET_RR_MIN; double rr = rrWant; double effSl = 0.0, effTp = 0.0; double reach = 0.0; for(;;) { reach = m_ladder.WinShare(idxList, n, sl, rr * sl, m_barrierHorizonBars, effSl, effTp); //--- Floor reached, or this ratio is reachable: either way the walk is over. BarrierStepDownRr //--- is strictly decreasing and bottoms at BARRIER_TARGET_RR_MIN, so this cannot spin. if(reach >= BarrierMinReachPct(rr) || rr <= BARRIER_TARGET_RR_MIN + 0.01) break; rr = BarrierStepDownRr(rr); } double tp = rr * sl; //--- Printed so a raise that was proposed and then walked back is visible as exactly that, not //--- as a rung that never wanted more. string rrNote = (rrWant > rr + 0.01) ? StringFormat(" [legs proposed 1:%.1f, unreached]", rrWant) : ""; int needH = RequiredHorizonBars(sl, tp); int gotH = GrantedHorizonBars(sl, tp); //--- DETECTABILITY AT THIS RUNG - the quantity BARRIER_SCALE_OBJECTIVE trades against width. //--- n_eff = sample / this rung's own measured lifespan; the smallest edge 2 sigma can //--- separate from chance follows, and multiplying by the width gives the smallest EV per //--- trade that could ever be PROVEN at this geometry. double rungLife = (m_ladder.LastRungLifespan() > 0.0) ? m_ladder.LastRungLifespan() : 1.0; double rungEffN = MathMax((double)n / rungLife, 2.0); double beP = 1.0 / (1.0 + rr); // THIS rung's break-even, not the floor's double rungSE = BinomialSEPct(beP, rungEffN); double minEdge = EDGE_MIN_SIGMAS * rungSE; // percentage points double minEV = minEdge / 100.0 * (sl + tp); // in ATR per trade //--- Round-trip spread as a share of the move. The pass-2 re-derivation applies it for real; //--- that is exactly what the fixed-point iteration is for. double costPct = (sl + tp > 0.0 && m_spreadAtr > 0.0) ? 100.0 * 2.0 * m_spreadAtr / (sl + tp) : 0.0; bool costOK = (m_spreadAtr <= 0.0 || costPct <= BARRIER_MAX_COST_FRACTION_PCT); //--- REJECT ON THE CEILING ONLY, matching ReportGeometryExpectancyScan's '!' exactly - that is the //--- whole point of applying one rule in two places. gotH < needH is the SEPARATE, milder //--- truncation the ladder's snap-down always imposes (317 needed -> 256 granted); it is reported, //--- not rejected, because rejecting on it would select rungs for landing just above a ladder point //--- rather than for anything about the market. The timeout share is what says whether it bites. bool fitsH = (needH <= EffectiveHorizonMax()); //--- The measured pair is printed beside the requested one whenever the grid could not express the //--- request, so a collision between two quantiles is visible as a collision rather than as two //--- rungs that happen to score identically. string effNote = ""; if(effSl > 0.0 && (MathAbs(effSl - sl) > 0.005 || MathAbs(effTp - tp) > 0.005)) effNote = StringFormat(" @grid %.2f/%.2f", effSl, effTp); rungRows += StringFormat("%sq%.0f(stop %.2f target %.2f=1:%.1f%s width %.2f reach %.1f%%%s needs %d gets %d" " | L %.0f -> n_eff %.0f, min provable edge %.1fpp = %.2f ATR/trade," " cost %.1f%%%s%s)", (rungRows == "" ? "" : " "), 100.0 * q, sl, tp, rr, rrNote, sl + tp, reach, effNote, needH, gotH, rungLife, rungEffN, minEdge, minEV, costPct, (costOK ? "" : " COST-REJECTED"), (fitsH ? "" : " CLAMPED-REJECTED")); //--- Both objectives share the reachability floor and the horizon ceiling; they differ only //--- in which end of the surviving set they take. bool eligible = fitsH && costOK && reach >= BarrierMinReachPct(rr); bool takeIt = (BARRIER_SCALE_OBJECTIVE == BARRIER_SCALE_DEPLOY) ? (slRaw <= 0.0) : true; if(eligible && takeIt) { slRaw = sl; tpRaw = tp; chosenQ = q; chosenReach = reach; chosenRr = rr; } } if(slRaw <= 0.0) { //--- No rung clears the floor: the horizon cannot deliver a 1:RR target at ANY survivable stop on //--- this instrument. Take the tightest rung (the most reachable one there is) and say so - the //--- ratio is risk policy, and the honest response is to price it, not to silently abandon it. double q = BARRIER_SL_QUANTILE_LADDER[BARRIER_SL_QUANTILE_COUNT - 1]; slRaw = slLadder[BARRIER_SL_QUANTILE_COUNT - 1]; if(slRaw < MIN_SL_ATR_MULTIPLIER) slRaw = MIN_SL_ATR_MULTIPLIER; //--- SAME STEP-DOWN AS THE LOOP, and leaving it out is what actually shipped the 20.7:1 //--- labels: this branch runs precisely when no rung was reachable, so re-proposing the raw //--- leg-implied ratio here re-applies the ratio that had just been rejected everywhere. double fbSl = 0.0, fbTp = 0.0; double rrWantFb = (m_swingMedianLegAtr > 0.0) ? BarrierSnapRr(m_swingMedianLegAtr / slRaw) : BARRIER_TARGET_RR_MIN; chosenRr = rrWantFb; for(;;) { chosenReach = m_ladder.WinShare(idxList, n, slRaw, chosenRr * slRaw, m_barrierHorizonBars, fbSl, fbTp); if(chosenReach >= BarrierMinReachPct(chosenRr) || chosenRr <= BARRIER_TARGET_RR_MIN + 0.01) break; chosenRr = BarrierStepDownRr(chosenRr); } tpRaw = chosenRr * slRaw; chosenQ = q; Print(ID + StringFormat(": WARNING - no stop quantile produced a %.1f:1 target reached on at least " "%.0f%% of bars inside the %d-bar horizon AND resolvable within the %d-bar " "ceiling. Taking the tightest rung (q%.0f) at %.1f%% reachability. The " "positive class will be rare and training will be correspondingly hard; " "raise BARRIER_HORIZON_MAX or lower BARRIER_TARGET_RR_MIN if that proves " "untrainable.", chosenRr, BarrierMinReachPct(chosenRr), m_barrierHorizonBars, EffectiveHorizonMax(), 100.0 * chosenQ, chosenReach)); } int meanBudget = 0; int cycleBars = MeasureCloseAllBudget(meanBudget); if(cycleBars > 0) Print(ID + StringFormat(": CLOSE-ALL BUDGET - the scheduled close-all flattens every position" " every %d bars, so no trade from this chart can live longer than that" " and an entry landing anywhere in the cycle gets %d bars on average." " The horizon ladder just granted %d. Measured 2026-08-22: EVERY label" " timeout on this chart was the close-all and NONE was the horizon, so" " the horizon is not the binding barrier - the close-all is, and the" " horizon ceiling is now this cycle rather than the %d-bar" " BARRIER_HORIZON_MAX, which never bound anything. A target needing" " more than %d bars is unreachable however reachable the excursion scan" " says it is, so the ladder is expected to pick a NARROWER pair - which" " is what the MEASURE objective wants anyway, since min provable EV" " grows as width squared.", cycleBars, meanBudget, m_barrierHorizonBars, BARRIER_HORIZON_MAX, meanBudget)); Print(ID + StringFormat(": barrier SCALE ladder - objective %s (ratio: policy MINIMUM 1:%.1f, raised per rung toward the median swing leg of %.2f*ATR where the stop leaves room). Every " "rung must clear 60%% of its OWN break-even in reachability (%.0f%% at the " "minimum ratio, looser above it), resolve inside the %d-bar horizon " "ceiling, and keep the round-trip spread under %.1f%% of its width; of those, " "%s. Width is EV per trade, narrowness is EV you can PROVE - min provable EV " "grows as width SQUARED because the label's lifespan does, so the two ends of " "this ladder are opposed and only one is right per phase. - %s | chose q%.0f, " "target reached on %.1f%% of bars; needs %d bars, granted %d (ceiling %d, then " "snapped DOWN - the shortfall shows up as the timeout share on the next " "label-cache line)", (BARRIER_SCALE_OBJECTIVE == BARRIER_SCALE_DEPLOY ? "DEPLOY (maximise EV per trade)" : "MEASURE (maximise detectability - the edge is not proven yet)"), BARRIER_TARGET_RR_MIN, m_swingMedianLegAtr, BarrierMinReachPct(BARRIER_TARGET_RR_MIN), EffectiveHorizonMax(), BARRIER_MAX_COST_FRACTION_PCT, (BARRIER_SCALE_OBJECTIVE == BARRIER_SCALE_DEPLOY ? "the WIDEST wins" : "the NARROWEST wins"), rungRows, 100.0 * chosenQ, chosenReach, RequiredHorizonBars(slRaw, tpRaw), GrantedHorizonBars(slRaw, tpRaw), EffectiveHorizonMax())); //--- 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). The model was //--- then trained to predict an outcome that essentially never happens. int travelTp = 0, travelSl = 0; for(int i = 0; i < n; i++) { if(up[i] >= tpRaw) travelTp++; if(dn[i] >= slRaw) travelSl++; } double tpTravel = 100.0 * travelTp / n; double slTravel = 100.0 * travelSl / 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; if(conditional) Print(ID + StringFormat(": geometry source - CONDITIONAL on the fractal label: MFE/MAE measured " "from each labeled bar's close over the leg to its NEXT fractal extreme " "(%d IS legs, entry-anchored), not over every bar. The stop/target below " "are sized for the bars the model actually trades.", n)); 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, chosen by the SCALE ladder above), target %.2f*ATR " "(= %.1f x the stop%s) | width %.2f*ATR | " "travelled within the %d-bar EXCURSION window: target on %.1f%% of bars, stop " "on %.1f%% (near-tautological - that is where the quantiles were read) | " "implied break-even %.1f%%. This does NOT create expectancy - chance precision " "equals break-even at every RATIO - what the geometry buys is WIDTH, and width " "is the EV multiplier because the spread is a fixed cost per trade.", //--- 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 * chosenQ, m_derivedTpMult, chosenRr, (chosenRr > BARRIER_TARGET_RR_MIN + 0.01 ? ", RAISED above the 1:2 policy floor by the median swing leg" : ", the policy MINIMUM ratio - the swings did not offer more"), m_derivedSlMult + m_derivedTpMult, m_swingMedianBars, tpTravel, slTravel, breakeven)); //--- THE THREE WINDOWS, printed together because two of them look like the same quantity and are //--- not. Nothing was broken. They measure different windows: if(!conditional && ArraySize(labUp) >= n && n > 0) { int excReach = 0, buyCount = 0; for(int i = 0; i < n; i++) { if(up[i] >= tpRaw) excReach++; if(labUp[i]) buyCount++; } double recSl = 0.0, recTp = 0.0; double ladderShare = m_ladder.WinShare(idxList, n, slRaw, tpRaw, m_barrierHorizonBars, recSl, recTp); double buyPct = 100.0 * buyCount / n; double gap = MathAbs(ladderShare - buyPct); //--- The gap tolerance has to scale with how badly the grid mis-states the pair, not sit at a //--- flat 5pp: the ladder measures recSl/recTp, the labels measure slRaw/tpRaw, and when //--- those differ the two are answering NEARLY the same question rather than exactly it. double gridSkew = (slRaw > 0.0 && tpRaw > 0.0 && recSl > 0.0) ? MathAbs((recTp / recSl) - (tpRaw / slRaw)) / (tpRaw / slRaw) : 0.0; double gapTol = 5.0 + 100.0 * gridSkew; Print(ID + StringFormat(": window reconciliation at stop %.2f target %.2f - EXCURSION window " "(%d bars, sizes the barrier): target travelled on %.1f%% of bars | " "BARRIER horizon (%d bars, what the trade lives in): ladder says a long " "wins %.1f%% (measured at the grid pair %.2f/%.2f, ratio %.2f vs the asked " "%.2f), the label cache says Buy %.1f%% | ladder-vs-label gap %.1fpp vs a " "%.1fpp tolerance %s. The first number is EXPECTED to be the smallest - it " "asks a %d-bar question where the other two ask a %d-bar one.", slRaw, tpRaw, m_swingMedianBars, 100.0 * excReach / n, m_barrierHorizonBars, ladderShare, recSl, recTp, (recSl > 0.0 ? recTp / recSl : 0.0), tpRaw / slRaw, buyPct, gap, gapTol, (gap <= gapTol ? "(rung discretisation, expected)" : "<-- TOO LARGE to be discretisation; the ladder and the label walk should" " be answering the identical question, so one of them is wrong"), m_swingMedianBars, m_barrierHorizonBars)); } //--- 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(); //--- The scale ladder already retreats until this floor is met, so reaching here means even its //--- tightest rung could not - which is a HORIZON problem, not a ratio problem. Same failure the //--- clamped-horizon incident produced, and the ladder report above shows every rung it tried. if(chosenReach < BarrierMinReachPct(chosenRr)) Print(ID + StringFormat(": WARNING - the target of %.2f*ATR (%.1f x the %.2f stop) is reached on " "only %.1f%% of bars inside the %d-bar horizon, and no rung of the scale " "ladder did better. The positive class will be that rare, so expect the " "recall floor to bite. Lengthen the horizon or lower BARRIER_TARGET_RR_MIN.", m_derivedTpMult, chosenRr, m_derivedSlMult, chosenReach, m_barrierHorizonBars)); return true; } //+------------------------------------------------------------------+ //| Mean bars-to-resolution over the label cache. 1.0 until something | //| has been measured, which makes EffectiveSampleSize() the identity | //| - the pre-2026-08-17 behaviour. That default is deliberate: an | //| UNMEASURED overlap must not silently shrink anyone's sample, so | //| the correction switches itself on only once it has evidence. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::MeanLabelLifespan(void) const { return m_labelOverlap.MeanLifespan(m_barrierHorizonBars); } //+------------------------------------------------------------------+ //| Independent observations behind `rawN` overlapping labels. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::EffectiveSampleSize(double rawN) const { return m_labelOverlap.EffectiveSampleSize(rawN, m_barrierHorizonBars); } //+------------------------------------------------------------------+ //| Bars this geometry needs before its label stops being truncated. | //| IDENTICAL arithmetic to ComputeBarrierHorizonBars() - see the | //| declaration for the 2026-08-17 divergence that made factoring it | //| out necessary rather than tidy. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::RequiredHorizonBars(double slMult, double tpMult) { double swing = (double)MathMax(m_swingMedianBars, 1); if(slMult <= 0.0 || tpMult <= 0.0) return BARRIER_HORIZON_MIN; return (int)MathRound(swing * slMult * tpMult); } //+------------------------------------------------------------------+ //| Bars between scheduled close-alls, and what an entry really gets. | //| | //| Returns the CYCLE length (the most any trade can live) and sets | //| meanBudgetBars to the mean over entries spread through the cycle, | //| which is what an average bar is labelled under. Measured off the | //| real bar series, so it is session- and DST-correct rather than | //| arithmetic on a nominal week. | //| | //| WHY (2026-08-22): every single label timeout on both live charts | //| was the close-all, none was the horizon - 14417 of 14417 on | //| USDJPY, 2434 of 2434 on SP500. The horizon ladder had granted 96 | //| bars to a trade that is flattened every Friday. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::MeasureCloseAllBudget(int &meanBudgetBars) { meanBudgetBars = 0; int cutSec = PeriodSeconds(m_period); if(cutSec <= 0) return 0; int bars = (int)MathMin(Bars(m_symbol.Name(), m_period), 4000); if(bars < 8) return 0; //--- Walk oldest -> newest, counting bars between the cut points the label walk itself would hit. int spans = 0, spanSum = 0, run = 0; datetime cut = 0; for(int i = bars - 1; i >= 0; i--) { datetime t = m_Time.GetData(i); if(t <= 0) continue; if(cut <= 0) { cut = NextScheduledCloseAll((datetime)(t + cutSec)); if(cut <= 0) return 0; // schedule off - the horizon really is the only barrier continue; } run++; if((datetime)(t + cutSec) > cut) { spans++; spanSum += run; run = 0; cut = NextScheduledCloseAll((datetime)(t + cutSec)); if(cut <= 0) break; } } if(spans <= 0) return 0; int cycle = (int)MathRound((double)spanSum / spans); //--- An entry lands uniformly inside the cycle, so it gets half of one on average. meanBudgetBars = (int)MathMax(1, MathRound(cycle / 2.0)); return cycle; } //+------------------------------------------------------------------+ //| The horizon ceiling, lowered to what the close-all really grants. | //| | //| BARRIER_HORIZON_MAX is 384 bars. The scheduled close-all flattens | //| every position on a ~29-bar cycle (H4, CLOSE_FRIDAY), so a label | //| granted more than that is describing a trade that cannot exist - | //| and the ladder was granting 128. Measured 2026-08-22: EVERY | //| timeout on both charts was the close-all, NONE was the horizon. | //| | //| The CYCLE, not the ~15-bar mean an average entry gets: a Monday | //| entry really does get the whole cycle, and rejecting on the mean | //| would invent a second rule where the design has one ceiling. | //| Measured once and cached - the scale ladder asks per rung. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::EffectiveHorizonMax(void) { if(m_closeAllCycleBars == 0) { //--- "Schedule off" is a PERMANENT answer and is cached. "Not enough bars loaded yet" is NOT - //--- caching that would silently restore the 384-bar ceiling for the whole process because one //--- early call happened before history arrived. if((int)targetDayOfWeek == -1 || (int)targetHour == -1 || (int)targetMinutes == -1) m_closeAllCycleBars = -1; else { int mean = 0; int cycle = MeasureCloseAllBudget(mean); if(cycle <= 0) return BARRIER_HORIZON_MAX; // not measurable yet - retry next call, cache nothing m_closeAllCycleBars = cycle; m_closeAllMeanBudget = mean; } } if(m_closeAllCycleBars <= 0) return BARRIER_HORIZON_MAX; // schedule off - the horizon really is the only barrier return (int)MathMax(BARRIER_HORIZON_MIN, MathMin(BARRIER_HORIZON_MAX, m_closeAllCycleBars)); } //+------------------------------------------------------------------+ //| THE horizon ladder, and the only copy of it. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::SnapHorizonToLadder(int rawBars) { //--- THE ladder array itself now lives in CTripleBarrier::SnapToLadder() - the one copy. Stays a //--- thin forwarder here because EffectiveHorizonMax() reads this signal's close-all cache, which //--- is exactly the chart-side state a pure geometry function must not see. return CTripleBarrier::SnapToLadder(rawBars, BARRIER_HORIZON_MIN, EffectiveHorizonMax()); } //+------------------------------------------------------------------+ //| What this pair would actually be labelled under - see the | //| declaration for why this is NOT RequiredHorizonBars(). | //+------------------------------------------------------------------+ int CExpertSignalAIBase::GrantedHorizonBars(double slMult, double tpMult) { return SnapHorizonToLadder(RequiredHorizonBars(slMult, tpMult)); } //+------------------------------------------------------------------+ //| Break-even INCLUDING the spread - see the declaration comment. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| See the declaration. The trade the EA would ACTUALLY have taken | //| from this bar, under the exit policy actually in force. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::SimulateTradeOutcome(int entryIdx, bool isLong, double &rMultiple, int &lifespanBars, bool &endedOnVote, bool &endedOnTimeout) { rMultiple = 0.0; endedOnTimeout = false; lifespanBars = 0; endedOnVote = false; double atr = m_ATR.Main(entryIdx); if(!MathIsValidNumber(atr) || atr <= 0.0) return false; double entry = m_Close.GetData(entryIdx); if(!MathIsValidNumber(entry) || entry <= 0.0) return false; double slMult, tpMult; BarrierMultiples(slMult, tpMult); double risk = slMult * atr; double reward = tpMult * atr; //--- Same broker-minimum widening as TripleBarrierLabel: this simulates the live trade, so it //--- wears the live constraints. ONE function now, not two hand-written copies - see //--- CTripleBarrier::ApplyMinStopWidening(). CTripleBarrier::ApplyMinStopWidening(risk, reward, TCMinStopDistance(m_symbol.Name())); if(risk <= 0.0) return false; double spread = (double)m_symbol.Spread() * m_symbol.Point(); if(!MathIsValidNumber(spread) || spread < 0.0) spread = 0.0; //--- THE SAME fill/barrier convention TripleBarrierLabel's walk uses - literally the same call now, //--- not a hand-copied one: if the two ever disagreed about what a trade costs, the "simulated vs //--- hold-to-barrier" comparison this function exists to produce would measure the discrepancy //--- between two pieces of our own arithmetic rather than the effect of the exit policy. See //--- CTripleBarrier::ComputeLevels(). double longTp, longSl, shortTp, shortSl; CTripleBarrier::ComputeLevels(entry, spread, risk, reward, longTp, longSl, shortTp, shortSl); double fill = isLong ? (entry + spread) : (entry - spread); double tpLevel = isLong ? longTp : shortTp; double slLevel = isLong ? longSl : shortSl; //--- Vote-reversal threshold, 0 when the policy has no vote-driven exit. A simulation that //--- models a different exit rule than the one that runs is worse than no simulation. bool voteExitsOn = (!m_exitHoldToBarrier && m_exitVoteThreshold > 0.0 && m_exitVoteThreshold <= 100.0 && ArraySize(m_oosDecisionSeries) > 0); int last = entryIdx - MathMax(m_barrierHorizonBars, 1); if(last < 0) last = 0; //--- THE SCHEDULED CLOSE-ALL IS THIS WALK'S SECOND VERTICAL BARRIER TOO (2026-08-21). int cutBarSec = PeriodSeconds(m_period); datetime simCut = NextScheduledCloseAll((datetime)(m_Time.GetData(entryIdx) + cutBarSec)); for(int t = entryIdx - 1; t >= last; t--) { //--- Ahead of the price reads, so lifespanBars keeps the last bar actually HELD and the fall- //--- through below closes there. Identical placement to the label's cut, deliberately. if(simCut > 0 && (datetime)(m_Time.GetData(t) + cutBarSec) > simCut) break; double hi = m_High.GetData(t); double lo = m_Low.GetData(t); double cl = m_Close.GetData(t); if(!MathIsValidNumber(hi) || !MathIsValidNumber(lo) || hi == EMPTY_VALUE || lo == EMPTY_VALUE) break; lifespanBars = entryIdx - t; //--- STOP FIRST on a bar that spans both, same pessimism as the label walk. if(isLong ? (lo <= slLevel) : (hi >= slLevel)) { rMultiple = -1.0; return true; } if(isLong ? (hi >= tpLevel) : (lo <= tpLevel)) { rMultiple = reward / risk; return true; } //--- VOTE REVERSAL, checked AFTER the barriers on the same bar. Checking the vote first would //--- credit the exit policy with escapes that a real stop would have taken out of its hands. if(voteExitsOn && t < ArraySize(m_oosDecisionSeries)) { double vote = m_oosDecisionSeries[t]; bool reversed = isLong ? (vote < 0.0) : (vote > 0.0); if(reversed && MathAbs(vote) >= m_exitVoteThreshold && MathIsValidNumber(cl) && cl > 0.0) { //--- Closed at THIS bar's close, at whatever P&L that is - which is the whole point: a //--- vote exit produces a CONTINUOUS payoff, not a win or a loss, and that is why an //--- exit-aware gate cannot go on scoring win-rate against a fixed break-even. rMultiple = isLong ? (cl - fill) / risk : (fill - cl) / risk; endedOnVote = true; return true; } } } //--- Neither barrier touched: the trade is closed at the last bar the walk could see. Three ways to //--- get here and they are one economic event - ran out of horizon, ran off loaded history, or was //--- flattened by the scheduled close-all - so all three are counted as timeouts and paid at //--- whatever the close was, exactly as the live EA would have. int lastSeen = entryIdx - MathMax(lifespanBars, 1); if(lastSeen < 0) lastSeen = 0; double closeOut = m_Close.GetData(lastSeen); if(!MathIsValidNumber(closeOut) || closeOut <= 0.0) return false; rMultiple = isLong ? (closeOut - fill) / risk : (fill - closeOut) / risk; endedOnTimeout = true; return true; } //+------------------------------------------------------------------+ //| See the declaration. Replays this era's OOS calls under the exit | //| policy actually in force, and says how far that lands from the | //| hold-to-barrier outcome the gate certifies. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::SimulateExitPolicyOutcomes(void) { int n = ArraySize(m_oosDecisionSeries); for(int r = 0; r < n; r++) { //--- One full forward price walk per directional call, run in a single unchunked pass at the //--- end of an era - the longest thing between the last pass-3 yield and Train() returning. if(ShutdownRequested()) { m_simRSum = 0.0; m_simRSumSq = 0.0; m_simTrades = 0; m_simVoteExits = 0; m_simBarrierWins = 0; m_simTpHits = 0; //--- ALL TEN, not just the count. This line used to be `m_geoTrades = 0;` while nine //--- partial sums survived the abort, so the next era divided a stale numerator by a //--- restarted denominator - in exactly the paired-difference arithmetic that failed its //--- own acceptance test in b5e22a1. m_geo.Reset(); m_simTimeouts = 0; m_simTimeoutRSum = 0.0; return; } double vote = m_oosDecisionSeries[r]; if(vote == 0.0) continue; // abstained - no trade to replay bool isLong = (vote > 0.0); double rMult = 0.0; int life = 0; bool onVote = false, onTimeout = false; if(!SimulateTradeOutcome(r, isLong, rMult, life, onVote, onTimeout)) continue; m_simRSum += rMult; m_simRSumSq += rMult * rMult; m_simTrades++; //--- Same call, scored under the incumbent pair AND the pair this bar's excursion head would //--- have chosen. Rides this walk rather than opening its own so the two populations cannot //--- differ - see ReportCandidateGeometry. ScoreCandidateGeometry(r, isLong); if(onVote) m_simVoteExits++; //--- The population CostAdjustedBreakEvenPct assumes away - see EmpiricalBreakEvenPct. if(onTimeout) { m_simTimeouts++; m_simTimeoutRSum += rMult; } //--- What the CERTIFICATE counts on this same call, so the two are compared on identical //--- trades rather than on two different populations. bool barrierWin = (isLong ? (r < ArraySize(m_winLongCache) && m_winLongCache[r]) : (r < ArraySize(m_winShortCache) && m_winShortCache[r])); if(barrierWin) m_simBarrierWins++; //--- Target before stop in the SIMULATION. A non-vote non-timeout outcome is exactly -1.0 or //--- +reward/risk, so the sign identifies it without a fourth out-param. if(!onVote && !onTimeout && rMult > 0.0) m_simTpHits++; } //--- Latch this completed era's measurement for the next one to read - see m_lastTimeoutShare. if(m_simTrades > 0) { m_lastTimeoutShare = (double)m_simTimeouts / m_simTrades; m_lastTimeoutMeanR = (m_simTimeouts > 0) ? m_simTimeoutRSum / m_simTimeouts : 0.0; } } //+------------------------------------------------------------------+ //| See the declaration. The one line that says whether the number | //| being certified is still the number that would be traded. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ReportExitPolicyDivergence(void) { if(m_simTrades <= 0) return; bool policyIsBarrier0 = (m_exitHoldToBarrier || m_exitVoteThreshold <= 0.0); //--- Every era while vote exits are ON, because then this is load-bearing and its drift is the //--- thing to watch. Every quantity on this line is MEASURED and moves with geometry and //--- volatility, so it needs a cadence, not one print. if(policyIsBarrier0 && m_exitReplayReported && !TrainLogDue()) return; m_exitReplayReported = true; double meanR = m_simRSum / m_simTrades; //--- SE on the R DISTRIBUTION, not a binomial: once a vote exit can end a trade anywhere between //--- the two barriers the payoff is continuous, so "win rate vs break-even" stops being the //--- right statistic and expectancy-in-R vs 0 replaces it. double varR = (m_simRSumSq / m_simTrades) - (meanR * meanR); if(varR < 0.0) varR = 0.0; double effN = EffectiveSampleSize((double)m_simTrades); double seR = (effN > 0.0) ? MathSqrt(varR / effN) : 0.0; double barrierWinPct = 100.0 * (double)m_simBarrierWins / m_simTrades; //--- The SAME question answered by the other walk. Equal is the contract; the difference is the //--- only thing that can tell the operator the two have drifted apart again. double simWinPct = 100.0 * (double)m_simTpHits / m_simTrades; double votePct = 100.0 * (double)m_simVoteExits / m_simTrades; bool policyIsBarrier = (m_exitHoldToBarrier || m_exitVoteThreshold <= 0.0); //--- THE BAR THIS EXPECTANCY ACTUALLY CROSSES. Measured 2026-08-21 on SP500 H4: the replay //--- crossed zero at an implied 33.3% against a frictionless 33.24%. double slMultBe = 0.0, tpMultBe = 0.0; BarrierMultiples(slMultBe, tpMultBe); double frictionlessBePct = (slMultBe + tpMultBe > 0.0) ? 100.0 * slMultBe / (slMultBe + tpMultBe) : 50.0; //--- THE BREAK-EVENS, side by side, because they disagree and only one of them is measured. //--- t and m are the numbers CostAdjustedBreakEvenPct cannot see - see EmpiricalBreakEvenPct. double timeoutPct = 100.0 * (double)m_simTimeouts / m_simTrades; double timeoutMeanR = (m_simTimeouts > 0) ? m_simTimeoutRSum / m_simTimeouts : 0.0; PrintFormat("%s: EXIT-POLICY REPLAY of this era's %d OOS calls - policy in force: %s | simulated" " expectancy %+.3f R (2 SE %.3f on %.0f independent trades) | %.0f%% closed by a VOTE" " REVERSAL before either barrier | target-before-stop %.1f%% in THIS walk vs %.1f%%" " in the LABEL on the SAME calls (%+.1fpp; two walks, and anything but ~0 means they" " have drifted apart again - the expectancy above is this walk's)" " | BREAK-EVEN frictionless %.1f%% vs cost-adjusted %.1f%% vs horizon-aware %.1f%%" " (%.1f%% of trades reached NEITHER barrier and paid %+.3f R each). The R here is" " P&L/risk with the barriers placed off the FILL, so a win pays exactly TP/SL and a" " loss exactly -1: the frictionless figure is the one this expectancy crosses zero at." " %s", ID, m_simTrades, policyIsBarrier ? "SL/TP only (no vote exit)" : "vote exit ENABLED", meanR, 2.0 * seR, effN, votePct, simWinPct, barrierWinPct, simWinPct - barrierWinPct, frictionlessBePct, CostAdjustedBreakEvenPct(), EmpiricalBreakEvenPct(), timeoutPct, timeoutMeanR, policyIsBarrier ? "Vote exits are off, so every trade here resolved at a barrier or at a vertical one," " and the two walks should now agree call for call." : "VOTE EXITS ARE ON, so these are NOT the trades the deploy gate's win rate describes:" " that number grades target-before-stop, and a vote flip inside the horizon is neither." " Read the expectancy, not the win rate - a win rate over trades with continuous" " payoffs has no fixed break-even to be measured against."); } double CExpertSignalAIBase::CostAdjustedBreakEvenPct(void) { double slMult = 0.0, tpMult = 0.0; BarrierMultiples(slMult, tpMult); double frictionless = (slMult + tpMult > 0.0) ? 100.0 * slMult / (slMult + tpMult) : 50.0; if(!MathIsValidNumber(m_spreadAtr) || m_spreadAtr <= 0.0) return frictionless; //--- A long fills at close+spread, so its target needs (TP - spread) of net travel to pay and its stop //--- costs (SL + spread) when it trips. Same convention ReportGeometryExpectancyScan prices its ladder //--- with, so the two reports cannot disagree about what a trade costs. double reward = tpMult - m_spreadAtr; double risk = slMult + m_spreadAtr; if(reward <= 0.0 || risk <= 0.0) return frictionless; // target inside the spread - not tradeable at any hit rate return 100.0 * risk / (risk + reward); } //+------------------------------------------------------------------+ 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. 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. 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; } } //+------------------------------------------------------------------+ //| INTELLIGENT trade direction: measure the drift, pick the | //| side(s). The label cache's Buy/Sell shares ARE the win rates of | //| taking every bar long/short at the REAL geometry with costs | //| charged - their gap is the drift at this exact geometry. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::RefreshDriftVerdict(void) { //--- THE WIN CACHES, not the collapsed labels. This line reports always-long vs always-short win //--- rates, and that is what m_winLongCache/m_winShortCache hold - each side scored on its own //--- barriers, published before the collapse. int buys = 0, sells = 0, totalLbl = 0; int scanBars = MathMin(m_labelCacheBars, ArraySize(m_labelCacheHasValue)); scanBars = MathMin(scanBars, MathMin(ArraySize(m_winLongCache), ArraySize(m_winShortCache))); for(int i = 0; i < scanBars; i++) { if(!m_labelCacheHasValue[i]) continue; totalLbl++; if(m_winLongCache[i]) buys++; if(m_winShortCache[i]) sells++; } if(totalLbl <= 0) return; double effN = EffectiveSampleSize((double)totalLbl); if(effN < 30.0) return; // too little independent evidence to call a drift - stay/remain fail-open double pL = (double)buys / totalLbl; double pS = (double)sells / totalLbl; double seL = 100.0 * MathSqrt(MathMax(pL * (1.0 - pL), 0.0) / effN); double seS = 100.0 * MathSqrt(MathMax(pS * (1.0 - pS), 0.0) / effN); //--- Independence assumed, which is CONSERVATIVE here: a bar where both sides reached their target //--- counts on both, so the rates are positively correlated and the true SE of their gap is smaller. double seGap = MathSqrt(seL * seL + seS * seS); double gapPp = 100.0 * (pL - pS); double breakEven = CostAdjustedBreakEvenPct(); TRADING_DIRECTION driftVerdict = BOTH; if(MathAbs(gapPp) >= 2.0 * seGap) { if(gapPp > 0.0 && 100.0 * pS < breakEven) driftVerdict = LONG_ONLY; else if(gapPp < 0.0 && 100.0 * pL < breakEven) driftVerdict = SHORT_ONLY; } if(driftVerdict != g_warriorDriftVerdict || !g_warriorDriftMeasured) { g_warriorDriftMeasured = true; g_warriorDriftVerdict = driftVerdict; PrintFormat("%s: INTELLIGENT direction verdict - always-long %.1f%% vs always-short %.1f%%" " at this geometry (gap %+.1fpp, 2SE band %.1fpp on %.0f effective samples," " break-even %.1f%%) -> %s.%s", ID, 100.0 * pL, 100.0 * pS, gapPp, 2.0 * seGap, effN, breakEven, driftVerdict == LONG_ONLY ? "LONG only" : (driftVerdict == SHORT_ONLY ? "SHORT only" : "both sides"), (tradingdirection == DIRECTION_INTELLIGENT) ? "" : " (informational: the Trade direction input is not Intelligent, so this gates nothing)"); } } //+------------------------------------------------------------------+ //| First scheduled close-all strictly AFTER `after` (server time), | //| or 0 when the schedule is disabled. Mirrors the live check in | //| CExpertCustom::OnTick exactly: same three inputs, same -1 | //| disabled sentinels, same CLOSE_EVERYDAY semantics, same server | //| clock (bar times ARE server time). | //+------------------------------------------------------------------+ datetime CExpertSignalAIBase::NextScheduledCloseAll(const datetime after) { if((int)targetDayOfWeek == -1 || (int)targetHour == -1 || (int)targetMinutes == -1) return 0; // schedule off = no cutoff, exactly like live datetime dayStart = after - (after % 86400); for(int d = 0; d <= 7; d++) { datetime candDay = dayStart + d * 86400; MqlDateTime cdt; TimeToStruct(candDay, cdt); if(targetDayOfWeek != CLOSE_EVERYDAY && cdt.day_of_week != (int)targetDayOfWeek) continue; datetime cand = 0; if(targetHour == CH_MARKET_CLOSE) { //--- The symbol's CURRENT session table stands in for every historical day - MT5 keeps no //--- session history. ANALYSIS (2026-08-19): the bars themselves bound the error. Never //--- optimistic, so it cannot manufacture edge. int mktClose = WarriorMarketCloseSeconds(m_symbol.Name(), cdt.day_of_week); if(mktClose <= 0) continue; int cutSec = mktClose - (int)targetMinutes * 60; if(cutSec < 0) cutSec = 0; cand = candDay + cutSec; } else cand = candDay + (int)targetHour * 3600 + (int)targetMinutes * 60; if(cand > after) return cand; } return 0; } //+------------------------------------------------------------------+ //| 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. | //+------------------------------------------------------------------+ ENUM_SIGNAL CExpertSignalAIBase::TripleBarrierLabel(int idx) { //--- CLEARED FIRST, ahead of every early return below. m_lastExcUp = 0.0; m_lastExcDown = 0.0; m_lastTermTravel = 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_lastLabelWeekendCut = false; m_lastBarrierBothWon = false; m_lastBarrierBothWonTied = false; m_lastWinLong = false; m_lastWinShort = false; //--- Cleared with the rest, and for the same reason: an early return must not leave the PREVIOUS bar's //--- lifespan for the prebuild to accumulate. 0 = not measured, which the accumulator skips. m_lastLabelLifespan = 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; //--- THE BROKER'S MINIMUM STOP DISTANCE APPLIES TO THE LABEL (2026-08-19, user directive: //--- training goes through the same checks as trading). Irrelevant on H4 (0.5*ATR dwarfs any //--- stops level), real on M5/tight-ATR symbols. CTripleBarrier::ApplyMinStopWidening(risk, reward, TCMinStopDistance(m_symbol.Name())); //--- 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. THE SAME //--- arithmetic SimulateTradeOutcome uses - one copy now, see CTripleBarrier::ComputeLevels(). double longTp, longSl, shortTp, shortSl; CTripleBarrier::ComputeLevels(entry, spread, risk, reward, longTp, longSl, shortTp, shortSl); 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; //--- AGE (idx - t, so bars AFTER entry) at which each side first resolved either way. The won-at //--- indices above cannot serve: they are bar indices rather than ages, and they say nothing about the //--- losing side, which is what fixes a Neutral label's lifespan. See m_lastLabelLifespan. int longEndAge = 0, shortEndAge = 0; //--- Excursion accumulators. 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). ArrayInitialize(m_lastLadderUpAt, 0); ArrayInitialize(m_lastLadderDownAt, 0); int upCursor = 0, dnCursor = 0; //--- Age of the LAST bar the loop actually visited. Not simply the horizon: the walk breaks early when //--- it runs off loaded history, and a timeout lifespan of "the full horizon" would then be longer than //--- the window that was examined. int walkedAge = 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; //--- THE SCHEDULED CLOSE-ALL IS A SECOND VERTICAL BARRIER (2026-08-19). At the measured ~18-bar //--- mean lifespan on H4 (~3 days) a large share of labels straddled it. Schedule disabled = no //--- cutoff. int cutBarSec = PeriodSeconds(m_period); datetime weekendCut = NextScheduledCloseAll((datetime)(m_Time.GetData(idx) + cutBarSec)); for(int t = idx - 1; t >= last; t--) { if(weekendCut > 0 && (datetime)(m_Time.GetData(t) + cutBarSec) > weekendCut) { m_lastLabelWeekendCut = true; break; // the close-all flattens the book here - later bars do not exist for this trade } 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 //--- Overwritten every visited bar, so it ends up holding the LAST one - including when the //--- close-all break above fires, which is the mark that matters most. double cl = m_Close.GetData(t); if(MathIsValidNumber(cl) && cl != EMPTY_VALUE) m_lastTermTravel = (cl - entry) / atr; //--- Excursions accumulate only over the REFERENCE WINDOW, not the whole barrier horizon - //--- see m_swingMedianBars. 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. int age = idx - t; walkedAge = age; 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; longEndAge = age; } else if(hi >= longTp) { longWon = true; longWonAt = t; longEndAge = age; } } if(!shortWon && !shortLost) { if(hi >= shortSl) { shortLost = true; shortEndAge = age; } else if(lo <= shortTp) { shortWon = true; shortWonAt = t; shortEndAge = age; } } //--- 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. } 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); } //--- WHEN THIS LABEL BECAME KNOWABLE, which is what the overlap correction needs - see //--- m_lastLabelLifespan. So a bar with a winner is determined at that win, however long the other //--- side takes. if(longWon || shortWon) { if(longWon && shortWon) m_lastLabelLifespan = (int)MathMin(longEndAge, shortEndAge); else m_lastLabelLifespan = (longWon ? longEndAge : shortEndAge); } else if(longLost && shortLost) m_lastLabelLifespan = (int)MathMax(longEndAge, shortEndAge); else m_lastLabelLifespan = walkedAge; // a live side ran out of horizon: the timeout IS the decision //--- 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. m_lastWinLong = longWon; m_lastWinShort = shortWon; if(longWon && !shortWon) return Buy; if(shortWon && !longWon) return Sell; //--- BOTH TARGETS REACHED. 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). 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. | //+------------------------------------------------------------------+ int CExpertSignalAIBase::ComputeBarrierHorizonBars(int bars) { //--- The ladder itself now lives in SnapHorizonToLadder(), which this function ends by calling. //--- double rather than int so MathMedian can read it; the values are bar counts either way. double gaps[]; ArrayResize(gaps, 0); //--- LEG RANGE, harvested in the SAME pivot scan as the leg duration (2026-08-19). Two properties //--- of one object: how long a swing lasts and how far it travels. Measuring them together is what //--- keeps the horizon and the target describing the same legs instead of two different windows. double legs[]; ArrayResize(legs, 0); double prevPivotPrice = 0.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; double pivotPrice = m_ADZigZag.GetData(0, p); if(pivotPrice == 0.0) continue; if(prevPivot >= 0) { int gap = p - prevPivot; if(gap > 0) { int n = ArraySize(gaps); ArrayResize(gaps, n + 1); gaps[n] = gap; //--- ATR-NORMALISED so the median is a multiple comparable with the barrier multiples, //--- and read at the leg's own bar so an instrument whose volatility regime changed over //--- the sample contributes each leg on its own scale rather than on today's. double legAtr = m_ATR.Main(p); if(prevPivotPrice > 0.0 && MathIsValidNumber(legAtr) && legAtr > 0.0) { double range = MathAbs(prevPivotPrice - pivotPrice) / legAtr; if(range > 0.0 && MathIsValidNumber(range)) { int m = ArraySize(legs); ArrayResize(legs, m + 1); legs[m] = range; } } } } prevPivot = p; prevPivotPrice = pivotPrice; } 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) swingMedian = MathMedian(gaps); 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. For a driftless random walk leaving the band [-m*ATR, //--- +k*ATR], the expected first-passage time is proportional to m*k. 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. int legCount = ArraySize(legs); if(legCount >= BARRIER_HORIZON_MIN_SAMPLES) m_swingMedianLegAtr = MathMedian(legs); else m_swingMedianLegAtr = 0.0; 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". m_barrierHorizonClamped = (raw > EffectiveHorizonMax()); //--- Clamp + snap live in SnapHorizonToLadder() so the scale ladder can ask the same question about a //--- candidate rung without a second copy of the ladder - see its header. return SnapHorizonToLadder(raw); } //+------------------------------------------------------------------+ //| 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. 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. 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); if(IsFractalTarget()) Print(ID + ": FRACTAL TARGET active - the training label is the direction to the next confirmed" " 5-bar fractal extreme (min move max(2 spreads, 0.10 ATR), outside bars Neutral), NOT the" " barrier verdict. The barrier geometry above still sizes the live orders and the win-rate" " gate: deploy is decided on what a trade at that SL/TP actually collected."); } //+------------------------------------------------------------------+ //| 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. | //+------------------------------------------------------------------+ 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); //--- FRACTAL TARGET: the barrier walk above still runs in full - it fills the //--- excursion/ladder/win caches that the measured geometry, the expectancy scan and the era //--- gate's realized-win scoring all read - but the TRAINING label it returned is replaced by //--- the fractal-direction verdict. if(IsFractalTarget()) verdict = FractalDirectionLabel(idx); //--- IS-ONLY, matching the final tally pass exactly. 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) { //--- MEAN LABEL LIFESPAN, accumulated on the IS population for the same reason the timeout //--- share is: it deflates standard errors computed on that population. Accumulate() applies //--- the same lifespanBars > 0 guard this used to spell out at the call site. m_labelOverlap.Accumulate(m_lastLabelLifespan); if(verdict == Neutral && m_lastBarrierTimedOut) { m_labelPrebuildTimeoutCount++; if(m_lastLabelWeekendCut) m_labelPrebuildWeekendCutCount++; } //--- 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). All three of the //--- ladder's arrays go over in one call, so a partial row is not expressible here. m_ladder.StoreBar(idx, m_lastLadderUpAt, m_lastLadderDownAt, m_lastTermTravel); 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). | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+ //| FRACTAL-DIRECTION LABEL for one bar (TrainingTarget=TARGET_ | //| FRACTAL). Direction of price from bar idx's close to the NEXT | //| confirmed strict 5-bar fractal extreme - the reference library's | //| per-bar extremum-direction target, ~balanced by construction. | //+------------------------------------------------------------------+ ENUM_SIGNAL CExpertSignalAIBase::FractalDirectionLabel(int idx) { double entry = m_Close.GetData(idx); double atr = m_ATR.Main(idx); if(!MathIsValidNumber(entry) || entry <= 0.0 || !MathIsValidNumber(atr) || atr <= 0.0) return Neutral; double spread = (double)m_symbol.Spread() * m_symbol.Point(); if(!MathIsValidNumber(spread) || spread < 0.0) spread = 0.0; double minMove = MathMax(2.0 * spread, 0.10 * atr); //--- p walks FORWARD IN TIME (indices shrink toward now). A fractal at p needs the two newer //--- neighbours p-1/p-2 to exist, so the scan stops at p == 2; a bar closer to now than that has an //--- unconfirmable label and stays Neutral - same convention as the barrier's unresolved horizon. int deepest = idx - 1; int shallowest = MathMax(idx - SWING_SCAN_CAP_BARS, 2); //--- Leg extremes over every bar visited (the extreme bar included): the conditional MFE/MAE the //--- geometry derivation feeds on - travel measured over exactly the leg the label points at. double legHi = -DBL_MAX, legLo = DBL_MAX; for(int p = deepest; p >= shallowest; p--) { double h0 = m_High.GetData(p); double l0 = m_Low.GetData(p); if(h0 == EMPTY_VALUE || l0 == EMPTY_VALUE || !MathIsValidNumber(h0) || !MathIsValidNumber(l0)) return Neutral; // ran off loaded history before a marker confirmed if(h0 > legHi) legHi = h0; if(l0 < legLo) legLo = l0; bool up = h0 > m_High.GetData(p + 1) && h0 > m_High.GetData(p + 2) && h0 > m_High.GetData(p - 1) && h0 > m_High.GetData(p - 2); bool dn = l0 < m_Low.GetData(p + 1) && l0 < m_Low.GetData(p + 2) && l0 < m_Low.GetData(p - 1) && l0 < m_Low.GetData(p - 2); if(!up && !dn) continue; if(up && dn) return Neutral; // outside bar: both extremes, unorderable within OHLC ENUM_SIGNAL verdict; if(up) verdict = (h0 - (entry + spread) >= minMove) ? Buy : Neutral; else verdict = ((entry - spread) - l0 >= minMove) ? Sell : Neutral; //--- Record the labeled leg's conditional excursions - IS region, prebuild passes only, and //--- only until the geometry is derived and pinned (see m_fracLegFav's declaration comment). if(verdict != Neutral && !m_geometryDerived && m_labelPrebuildActive && idx >= MathMax(2, m_labelPrebuildOosCutoff)) { if(verdict == Buy) RecordFractalLegExcursion((legHi - entry) / atr, (entry - legLo) / atr); else RecordFractalLegExcursion((entry - legLo) / atr, (legHi - entry) / atr); } return verdict; } return Neutral; // no fractal inside the scan cap - dead-quiet stretch, nothing to aim at } //+------------------------------------------------------------------+ //| 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; //--- THE GAP IN THE TEARDOWN GUARDS (ad80e0b), found by the 2026-08-17 21:58 shutdown. Normally //--- that is a once-per-run cost and it does not matter. if(ShutdownRequested()) return; //--- A model that is still TRAINING sizes its window by the training rule, not by the saved study //--- watermark. 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)); //--- Clamped for TWO reasons, only one of which is about labels (see ServableBars()). So an //--- unclamped prebuild here would re-break the very feature block Train()'s clamp just //--- repaired, from a path that looks unrelated to it. if(!ResizeBuffers(barsNow) || !RefreshData()) { //--- NEVER SILENT AGAIN. MQL5's own "failed to get N bars" line was in the log the whole time //--- and belonged to a stack frame nothing connected to the prebuild. Say which depth, and //--- say it is fatal here. if(!m_prebuildBlockWarned) { m_prebuildBlockWarned = true; PrintFormat("%s: label prebuild BLOCKED - buffers would not prepare for %d bars. If MQL5" " printed 'failed to get %d bars' just above, a buffer is being sized beyond the" " %d bars this symbol actually has, and no era can start until that is fixed.", ID, barsNow, barsNow, Bars(m_symbol.Name(), PERIOD_CURRENT)); } return; // m_labelCachePrebuilt stays false, retried next call } int settled = SettledBars(barsNow, "label prebuild"); if(settled <= 0) return; // depth still moving - retried next call, same contract as the line above if(settled < barsNow) { barsNow = settled; if(!ResizeBuffers(barsNow) || !RefreshData()) return; } 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_labelPrebuildWeekendCutCount = 0; m_labelPrebuildBothWonCount = 0; m_labelPrebuildBothWonTieCount = 0; //--- Reset WITH the cache, not once per process: a rebuild follows a geometry or horizon change, and //--- lifespans measured under the old barrier answer a different question. Carrying them forward would //--- deflate the new geometry's standard errors by the old geometry's overlap. m_labelOverlap.Reset(); 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--) { //--- Already chunked at 80 ms, so this costs at most one chunk - but the tally pass below is NOT //--- chunked, and on a stop there is no reason to walk the rest of the window to reach it. //--- Resumable by construction: m_labelPrebuildIndex is written before returning either way. if(ShutdownRequested()) { m_labelPrebuildIndex = i; return; } 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(). 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. 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. 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. 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_labelPrebuildWeekendCutCount > 0 ? " (of which " + IntegerToString(m_labelPrebuildWeekendCutCount) + " ended by the scheduled close-all, not the horizon)" : "") + //--- LABEL OVERLAP, printed with the distribution because it is a property of the same //--- measurement and because every standard error downstream is divided by it. (m_labelOverlap.Count() > 0 ? StringFormat(" | mean label lifespan %.1f bars of a %d-bar horizon -> %d overlapping labels " "are worth ~%d independent ones (every SE below is sized on that)", MeanLabelLifespan(), m_barrierHorizonBars, (int)m_labelOverlap.Count(), (int)EffectiveSampleSize((double)m_labelOverlap.Count())) : "") + (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)")); //--- INTELLIGENT TRADE DIRECTION (2026-08-19, user request: "add an Intelligent option that lets //--- the geometry adjust for the drift" - the SQX EdgeFinder precedent). RefreshDriftVerdict(); //--- DERIVE THE GEOMETRY FROM WHAT WAS JUST MEASURED, then relabel under it. See //--- m_geometryAdopted for why the scan outranks this function rather than the reverse. if((m_eraCount == 0 || !m_geometryDerived) && !m_geometryAdopted && !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. A full day of training on the measured 3.33/1.62 pair resumed //--- as 2:6 the moment the terminal restarted. 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. 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. 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