//+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Excursion.mqh | //| AnimateDread | //| https://www.mql5.com | //| EXCURSION-SIZE HEAD - a SECOND, small network that predicts HOW | //| FAR price travels, never WHICH WAY. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Build the head's topology: input window -> one hidden dense -> 2 | //| x ladder sigmoid outputs. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::ExcursionBuildTopology(CArrayObj &topology) { CLayerDescription *desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) return false; desc.count = (int)m_historyBars * m_neuronsCount; desc.type = defNeuron; desc.activation = NONE; desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo; if(!topology.Add(desc)) { delete desc; return false; } desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) return false; desc.count = EXCURSION_HIDDEN_UNITS; desc.type = defNeuron; desc.activation = HiddenLayerActivation(); desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo; if(!topology.Add(desc)) { delete desc; return false; } desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) return false; //--- SIGMOID, and the count must stay != 3: backProp switches to the joint softmax+CCE gradient //--- at exactly 3 outputs, which is right for one mutually-exclusive class decision and wrong //--- here. desc.count = 2 * BARRIER_LADDER_COUNT; desc.type = defNeuron; desc.activation = SIGMOID; desc.optimization = (ENUM_OPTIMIZATION)m_optimizationAlgo; if(!topology.Add(desc)) { delete desc; return false; } return true; } //+------------------------------------------------------------------+ //| Create the head once per run. Returns false (quietly, once) when | //| the head cannot be built - the classifier must keep training | //| regardless, since this is an instrument bolted onto its run and | //| not a dependency of it. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::ExcursionEnsureHead(void) { if(!UseExcursionHead) return false; if(CheckPointer(m_excNet) != POINTER_INVALID) return true; if(m_excHeadFailed) return false; if(m_historyBars <= 0 || m_neuronsCount <= 0) return false; CArrayObj *topology = new CArrayObj(); if(CheckPointer(topology) == POINTER_INVALID) { m_excHeadFailed = true; return false; } if(!ExcursionBuildTopology(topology)) { delete topology; m_excHeadFailed = true; Print(ID + ": excursion head - could not build topology; the size predictor is disabled for this" " run. The classifier is unaffected."); return false; } m_excNet = new CNet(topology); delete topology; if(CheckPointer(m_excNet) == POINTER_INVALID) { m_excHeadFailed = true; return false; } //--- Per-sample updates. The classifier's mini-batch accumulation is scoped to its own pass 2 and //--- would silently apply here otherwise; this net is small enough that batching buys nothing. m_excNet.SetBatchSize(1); //--- Scratch buffers allocated ONCE. getResults takes CArrayDouble*& and news one when handed NULL, //--- so a local would allocate and leak (or need a delete) on every one of ~32k bars per era. if(CheckPointer(m_excTgt) == POINTER_INVALID) m_excTgt = new CArrayDouble(); if(CheckPointer(m_excOut) == POINTER_INVALID) m_excOut = new CArrayDouble(); if(CheckPointer(m_excTgt) == POINTER_INVALID || CheckPointer(m_excOut) == POINTER_INVALID) { m_excHeadFailed = true; return false; } ArrayInitialize(m_excBaseHits, 0); ArrayInitialize(m_excBrierHead, 0.0); ArrayInitialize(m_excBrierBase, 0.0); ArrayInitialize(m_excBrierHeadT, 0.0); ArrayInitialize(m_excOosHits, 0); //--- Trailing ring: horizon of hold-back plus the rolling window itself. ArrayResize(m_excTrailRing, (int)MathMax(m_barrierHorizonBars, 1) + EXCURSION_TRAIL_WINDOW); ArrayInitialize(m_excTrailRing, 0); ArrayInitialize(m_excTrailHits, 0); ArrayInitialize(m_excBrierTrail, 0.0); m_excTrailHead = 0; m_excTrailCount = 0; m_excTrailN = 0; m_excTrailScored = 0; m_excBaseTotal = 0; m_excScored = 0; m_excScoredD = 0; m_excDiffSum = 0.0; m_excDiffSumSq = 0.0; m_excTrailDiffSum = 0.0; m_excTrailDiffSumSq = 0.0; m_excMonoViol = 0; Print(ID + StringFormat(": excursion head created - %d inputs -> %d hidden -> %d outputs " "(P(reach rung) for %d up + %d down rungs). MEASUREMENT ONLY this build: it " "predicts how FAR price travels, never which way, and reports a skill score " "against the constant base rate that a fixed ATR multiple already assumes.", (int)m_historyBars * m_neuronsCount, EXCURSION_HIDDEN_UNITS, 2 * BARRIER_LADDER_COUNT, BARRIER_LADDER_COUNT, BARRIER_LADDER_COUNT)); return true; } //+------------------------------------------------------------------+ //| This bar's 16 binary targets, straight off the first-passage | //| ladder. Returns false when the bar has no measured ladder, which | //| must skip the sample rather than train it as all-zero - an | //| unmeasured bar and a bar price never moved on are the same array | //| contents and opposite facts. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::ExcursionTargets(int idx) { if(CheckPointer(m_excTgt) == POINTER_INVALID) return false; if(!m_ladder.Has(idx)) return false; if(idx >= ArraySize(m_labelCacheHasValue) || !m_labelCacheHasValue[idx]) return false; //--- Same "not measured" marker the MI sample uses: TripleBarrierLabel's early returns leave the //--- excursions cleared to zero, and price cannot genuinely travel zero in BOTH directions over a //--- whole horizon. Training on those rows would teach the head that a fifth of bars never move. if(idx < ArraySize(m_excUpCache) && idx < ArraySize(m_excDownCache) && m_excUpCache[idx] <= 0.0 && m_excDownCache[idx] <= 0.0) return false; //--- HARD 1/0, NOT the classifier's LABEL_SMOOTH_HIGH/LOW (0.9/0.05). Against a base rate of //--- 0.99 the arithmetic is forced before the net learns anything at all: m_excTgt.Clear(); for(int k = 0; k < BARRIER_LADDER_COUNT; k++) m_excTgt.Add(m_ladder.UpAge(idx, k) > 0 ? 1.0 : 0.0); for(int k = 0; k < BARRIER_LADDER_COUNT; k++) m_excTgt.Add(m_ladder.DownAge(idx, k) > 0 ? 1.0 : 0.0); return true; } //+------------------------------------------------------------------+ //| IS: one training step. Call while TempData still holds the | //| FEATURE window - i.e. after the classifier's feedForward and | //| BEFORE its getResults(), which overwrites TempData in place with | //| the output activations. That ordering constraint is the only | //| coupling between the two nets and it is why this takes no index | //| for the forward pass. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ExcursionTrainStep(int idx) { if(!ExcursionEnsureHead()) return; //--- STRIDE. One bar in EXCURSION_TRAIN_STRIDE keeps thousands of samples an era and cuts the //--- head's training dispatches by the same factor. m_excTrainTick++; if((m_excTrainTick % EXCURSION_TRAIN_STRIDE) != 0) return; if(!ExcursionTargets(idx)) return; ulong excT0 = GetMicrosecondCount(); if(!m_excNet.feedForward(TempData)) return; //--- Base rates accumulated from the SAME rows the head trains on - IS only. for(int k = 0; k < 2 * BARRIER_LADDER_COUNT; k++) if(m_excTgt.At(k) > 0.5) m_excBaseHits[k]++; m_excBaseTotal++; m_excNet.backProp(m_excTgt, 1.0); //--- Charged to its OWN accumulator. Until now the head's passes landed in the era line's "other" //--- bucket, which is how a 3.6x era-time regression read as an unexplained jump in a column nobody //--- attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. m_excUs += GetMicrosecondCount() - excT0; } //+------------------------------------------------------------------+ //| OOS: score one bar. Brier score (mean squared error on a | //| probability) for the head and for the constant base rate, summed | //| per rung so the report can show WHERE any skill lives - a head | //| that only predicts the near rungs is still useful for a stop and | //| useless for a target. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ExcursionScoreStep(int idx) { if(CheckPointer(m_excNet) == POINTER_INVALID || m_excBaseTotal <= 0) return; if(!ExcursionTargets(idx)) return; //--- DISJOINT WINDOWS ONLY - both the honest statistic AND the whole scoring cost. int hz = (int)MathMax(m_barrierHorizonBars, 1); bool disjoint = ((m_excScored % hz) == 0); if(!disjoint) { ExcursionTrailPush(); m_excScored++; return; } ulong excS0 = GetMicrosecondCount(); bool fwdOk = m_excNet.feedForward(TempData); if(fwdOk) m_excNet.getResults(m_excOut); m_excUs += GetMicrosecondCount() - excS0; if(!fwdOk || CheckPointer(m_excOut) == POINTER_INVALID || m_excOut.Total() < 2 * BARRIER_LADDER_COUNT) return; //--- MONOTONICITY. Reaching 3 ATR implies reaching 0.5 ATR, so P(reach k) must be non-increasing //--- in k. Counted, not corrected: the rate is the diagnostic that says whether the survival //--- parameterisation is holding together at all. for(int side = 0; side < 2; side++) for(int k = 1; k < BARRIER_LADDER_COUNT; k++) if(m_excOut.At(side * BARRIER_LADDER_COUNT + k) > m_excOut.At(side * BARRIER_LADDER_COUNT + k - 1) + 1e-9) { m_excMonoViol++; side = 2; // one violation per bar is enough to characterise it break; } //--- THIS WINDOW's paired Brier differences over the decision rungs, accumulated below and banked //--- once after the loop. One value per disjoint window is what turns the two skill scores into //--- estimates with a standard error - see m_excDiffSum. bool decMask[]; DecisionRungMask(decMask); double barDiff = 0.0, barTrailDiff = 0.0; for(int k = 0; k < 2 * BARRIER_LADDER_COUNT; k++) { double y = (m_excTgt.At(k) > 0.5) ? 1.0 : 0.0; double p = m_excOut.At(k); double b = (double)m_excBaseHits[k] / m_excBaseTotal; //--- k runs side-major over the ladder, so the rung is k modulo the ladder length. bool isDec = decMask[k % BARRIER_LADDER_COUNT]; //--- ORACLE CONTROL. This is the control that separates "the head predicts per bar" from "the //--- head learned a LEVEL nearer the OOS rate than the frozen IS constant". It peeks at the //--- test block by construction, so it is a control and never a headline. if(y > 0.5) m_excOosHits[k]++; double brHead = (p - y) * (p - y); double brBase = (b - y) * (b - y); m_excBrierHead[k] += brHead; m_excBrierBase[k] += brBase; if(isDec) barDiff += brBase - brHead; //--- Trailing climatology, scored on the SAME bars. Only once the window holds a usable sample - //--- before that it would be a handful of bars pretending to be a rate. if(m_excTrailN >= EXCURSION_TRAIL_MIN_N) { double tr = (double)m_excTrailHits[k] / m_excTrailN; double brTrail = (tr - y) * (tr - y); m_excBrierTrail[k] += brTrail; //--- and the HEAD's Brier on this same bar, so the incumbent race compares the two //--- predictors on an identical bar set - see m_excBrierHeadT's declaration comment. m_excBrierHeadT[k] += brHead; if(isDec) barTrailDiff += brTrail - brHead; } } //--- Banked per WINDOW, not per rung: the rungs of one bar are the same forecast read at different //--- distances, so treating them as separate observations would inflate the count by eight. m_excDiffSum += barDiff; m_excDiffSumSq += barDiff * barDiff; if(m_excTrailN >= EXCURSION_TRAIL_MIN_N) { m_excTrailScored++; m_excTrailDiffSum += barTrailDiff; m_excTrailDiffSumSq += barTrailDiff * barTrailDiff; } ExcursionTrailPush(); m_excScoredD++; // every bar reaching here IS a disjoint one now m_excScored++; } //+------------------------------------------------------------------+ //| Advance the trailing-climatology ring by one bar. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ExcursionTrailPush(void) { int ringSize = ArraySize(m_excTrailRing); if(ringSize <= 0 || CheckPointer(m_excTgt) == POINTER_INVALID) return; int hz = (int)MathMax(m_barrierHorizonBars, 1); //--- Pack this bar's 32 outcomes into one mask. ulong mask = 0; for(int k = 0; k < 2 * BARRIER_LADDER_COUNT; k++) if(m_excTgt.At(k) > 0.5) mask |= ((ulong)1 << k); //--- The entry that just crossed from unresolved into the window, and the one falling out the far //--- end, are both at fixed offsets behind the write head - so each push is O(rungs), not O(window). if(m_excTrailCount >= hz) { int justResolved = ((m_excTrailHead - hz) % ringSize + ringSize) % ringSize; ulong rm = m_excTrailRing[justResolved]; for(int k = 0; k < 2 * BARRIER_LADDER_COUNT; k++) if((rm & ((ulong)1 << k)) != 0) m_excTrailHits[k]++; m_excTrailN++; } if(m_excTrailCount >= ringSize) { ulong om = m_excTrailRing[m_excTrailHead]; // about to be overwritten: it leaves the window for(int k = 0; k < 2 * BARRIER_LADDER_COUNT; k++) if((om & ((ulong)1 << k)) != 0) m_excTrailHits[k]--; m_excTrailN--; } m_excTrailRing[m_excTrailHead] = mask; m_excTrailHead = (m_excTrailHead + 1) % ringSize; if(m_excTrailCount < ringSize) m_excTrailCount++; } //+------------------------------------------------------------------+ //| Rungs whose Brier the decision actually depends on: the ones | //| bracketing the live stop and target, because ExcursionQuantile | //| interpolates between exactly those. Skill at 5 ATR is skill | //| about a distance no order is placed at, and quoting the best | //| rung of eight is a best-of-N over a grid. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::DecisionRungMask(bool &mask[]) { ArrayResize(mask, BARRIER_LADDER_COUNT); double slMult, tpMult; BarrierMultiples(slMult, tpMult); for(int k = 0; k < BARRIER_LADDER_COUNT; k++) { bool bracketsTp = (k + 1 < BARRIER_LADDER_COUNT && BARRIER_LADDER[k] <= tpMult && BARRIER_LADDER[k + 1] >= tpMult) || (k > 0 && BARRIER_LADDER[k - 1] <= tpMult && BARRIER_LADDER[k] >= tpMult); bool bracketsSl = (k + 1 < BARRIER_LADDER_COUNT && BARRIER_LADDER[k] <= slMult && BARRIER_LADDER[k + 1] >= slMult) || (k > 0 && BARRIER_LADDER[k - 1] <= slMult && BARRIER_LADDER[k] >= slMult); mask[k] = (bracketsTp || bracketsSl); } } //+------------------------------------------------------------------+ //| Reset the per-era scoring accumulators. Base rates are NOT reset | //| here - they are a property of the data, they only get more | //| precise with more eras, and re-estimating them from scratch every | //| era would make the baseline noisier than the thing it is meant to | //| be a floor for. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ExcursionResetEraScores(void) { for(int k = 0; k < 2 * BARRIER_LADDER_COUNT; k++) { m_excBrierHead[k] = 0.0; m_excBrierBase[k] = 0.0; m_excBrierHeadT[k] = 0.0; m_excOosHits[k] = 0; m_excBrierTrail[k] = 0.0; m_excTrailHits[k] = 0; } m_excScored = 0; m_excScoredD = 0; m_excDiffSum = 0.0; m_excDiffSumSq = 0.0; m_excTrailDiffSum = 0.0; m_excTrailDiffSumSq = 0.0; m_excMonoViol = 0; m_excUs = 0; //--- The trailing RING IS cleared here (2026-08-11; it deliberately was not, as "a rolling //--- estimate of the market, not of the era"). if(ArraySize(m_excTrailRing) > 0) ArrayInitialize(m_excTrailRing, 0); m_excTrailHead = 0; m_excTrailCount = 0; m_excTrailN = 0; m_excTrailScored = 0; } //+------------------------------------------------------------------+ //| The (stop, target) pair this bar's excursion head would choose. | //| | //| Same rule the GLOBAL derivation uses, applied per bar instead of | //| once per era: stop at a high quantile of ADVERSE travel so only a | //| minority of bars reach it, target at the median of FAVOURABLE | //| travel so it is reached about half the time. Which side is which | //| depends on the direction being taken. | //| | //| Neither creates expectancy - chance precision equals break-even | //| at every geometry. What varies per candidate is the BREAK-EVEN, | //| which is why the caller scores R and never a win rate. | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::CandidateGeometryFor(const int barIdx, const bool isLong, int &slRung, int &tpRung) { slRung = -1; tpRung = -1; if(CheckPointer(m_excNet) == POINTER_INVALID || m_excBaseTotal <= 0) return false; if(!ExcursionTargets(barIdx)) return false; if(!m_excNet.feedForward(TempData)) return false; //--- Favourable travel is UP for a long and DOWN for a short; the stop reads the other side. double favour = ExcursionQuantile(isLong, BARRIER_TP_QUANTILE); double adverse = ExcursionQuantile(!isLong, BARRIER_SL_QUANTILE); if(favour <= 0.0 || adverse <= 0.0) return false; //--- THE SAME TWO FLOORS THE GLOBAL DERIVATION APPLIES, for the same reasons: a stop tighter //--- than the broker minimum cannot be placed, and a ratio under the policy minimum buys a high //--- win rate at a break-even nothing downstream was set against. Without these the head chose //--- 2.00/1.00 on USDJPY - break-even 67% - which is c3daded in miniature: a selector optimising //--- its own criterion, unconstrained by the decision criterion. if(adverse < MIN_SL_ATR_MULTIPLIER) adverse = MIN_SL_ATR_MULTIPLIER; if(favour < adverse * BARRIER_TARGET_RR_MIN) favour = adverse * BARRIER_TARGET_RR_MIN; slRung = CFirstPassageLadder::RungFor(adverse); tpRung = CFirstPassageLadder::RungFor(favour); //--- Snapping is per leg, so the ratio can survive the quantiles and still be lost to the rungs. if(slRung >= 0 && tpRung >= 0 && BARRIER_LADDER[tpRung] < BARRIER_LADDER[slRung] * BARRIER_TARGET_RR_MIN) for(int k = tpRung + 1; k < BARRIER_LADDER_COUNT; k++) if(BARRIER_LADDER[k] >= BARRIER_LADDER[slRung] * BARRIER_TARGET_RR_MIN) { tpRung = k; break; } return (slRung >= 0 && tpRung >= 0); } //+------------------------------------------------------------------+ //| One OOS call, scored under both geometries on the SAME bar. | //| | //| Paired, and both legs resolved from the SAME ladder. Mixing the | //| price walk with the ladder here would measure the discrepancy | //| between two of our own evaluators rather than the effect of the | //| geometry - which is exactly what f8ac10c had to unpick one layer | //| over, where a label win rate sat beside a simulated expectancy. | //| | //| A bar unresolved under either pair contributes 0 R for that pair | //| and is COUNTED, because a candidate that resolves more often is | //| an advantage the mean would otherwise hide. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ScoreCandidateGeometry(const int barIdx, const bool isLong) { //--- See SGeometryScan::startTick. The clock starts on the first ATTEMPT, not the first success - a bar the //--- head cannot answer for still costs a forward pass. After the budget this simply stops //--- contributing, leaving the exit replay it rides on untouched. if(m_geo.startTick == 0) m_geo.startTick = GetTickCount(); else if(GetTickCount() - m_geo.startTick >= GEOMETRY_BUDGET_MS) return; //--- INCUMBENT PAIR, converted into ladder TRAVEL. The scan's mapping is risk = ladder + spread and //--- reward = ladder - spread, so the two legs convert with OPPOSITE signs: a stop trips after //--- (risk - spread) of travel, a target pays after (reward + spread). The candidate legs need no //--- conversion - ExcursionQuantile already reads the curve in ladder units. double slMult, tpMult; BarrierMultiples(slMult, tpMult); int incSl = CFirstPassageLadder::RungFor(slMult - m_spreadAtr); int incTp = CFirstPassageLadder::RungFor(tpMult + m_spreadAtr); if(incSl < 0 || incTp < 0) return; // no incumbent to compare against - scoring one leg alone would be a false baseline int candSl, candTp; if(!CandidateGeometryFor(barIdx, isLong, candSl, candTp)) return; double rInc = 0.0, rCand = 0.0; bool incTo = false, candTo = false; //--- BOTH must be evaluable or the bar is dropped whole: scoring one leg and defaulting the //--- other is the free-zero bug in a smaller costume. if(!m_ladder.OutcomeR(barIdx, isLong, incSl, incTp, m_spreadAtr, rInc, incTo)) return; if(!m_ladder.OutcomeR(barIdx, isLong, candSl, candTp, m_spreadAtr, rCand, candTo)) return; if(incTo) m_geo.incOpen++; if(candTo) m_geo.candOpen++; double d = rCand - rInc; m_geo.diffSum += d; m_geo.diffSumSq += d * d; m_geo.incSum += rInc; m_geo.candSum += rCand; m_geo.candSl += BARRIER_LADDER[candSl]; m_geo.candTp += BARRIER_LADDER[candTp]; m_geo.trades++; } //+------------------------------------------------------------------+ //| Does per-candidate geometry beat the one global pair? | //| | //| MEASUREMENT ONLY - nothing here changes an order. Reported in R | //| and never as a win rate, because the whole point is that the | //| break-even moves per candidate, so no fixed bar exists to score a | //| win rate against. | //| | //| The SE is deflated by the label overlap on the same doctrine as | //| every other SE here: these calls are consecutive bars, not | //| independent trades. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ReportCandidateGeometry(void) { if(m_geo.trades < 2 || !TrainLogDue()) return; double mean = m_geo.diffSum / m_geo.trades; double var = (m_geo.diffSumSq / m_geo.trades) - (mean * mean); if(var < 0.0) var = 0.0; double effN = EffectiveSampleSize((double)m_geo.trades); double se = (effN > 0.0) ? MathSqrt(var / effN) : 0.0; double t = (se > 0.0) ? mean / se : 0.0; double slMult, tpMult; BarrierMultiples(slMult, tpMult); PrintFormat("%s: CANDIDATE GEOMETRY - %d OOS calls scored under BOTH pairs on the same bars, both" " resolved from the first-passage ladder | incumbent stop %.2f target %.2f -> %+.3f R" " | per-candidate mean stop %.2f target %.2f -> %+.3f R | difference %+.3f R at %.2f" " sigma on %.0f independent calls | timed out and MARKED AT THE HORIZON CLOSE:" " incumbent %.1f%%, candidate %.1f%% (marked, NOT scored 0 - a free zero would let the" " widest candidate win by never resolving, which is what this line first measured) | covered %d of this era's %d replayed calls%s. MEASUREMENT ONLY - no" " order uses this. Below 2 sigma the one global pair is doing as well, and it costs no" " forward pass.", ID, m_geo.trades, slMult, tpMult, m_geo.incSum / m_geo.trades, m_geo.candSl / m_geo.trades, m_geo.candTp / m_geo.trades, m_geo.candSum / m_geo.trades, mean, t, effN, 100.0 * m_geo.incOpen / m_geo.trades, 100.0 * m_geo.candOpen / m_geo.trades, m_geo.trades, m_simTrades, (m_geo.trades < m_simTrades ? StringFormat(" (stopped at the %.0f s budget)", GEOMETRY_BUDGET_MS / 1000.0) : "")); } //+------------------------------------------------------------------+ //| Per-bar quantile in ATR multiples, read off the predicted | //| survival curve: the largest rung whose reach-probability is | //| still >= (1 - tau), linearly interpolated between rungs. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::ExcursionQuantile(bool upward, double tau) { if(CheckPointer(m_excNet) == POINTER_INVALID) return -1.0; m_excNet.getResults(m_excOut); if(CheckPointer(m_excOut) == POINTER_INVALID || m_excOut.Total() < 2 * BARRIER_LADDER_COUNT) return -1.0; int off = upward ? 0 : BARRIER_LADDER_COUNT; double want = 1.0 - tau; // P(reach) at the quantile we are asking for double prev = BARRIER_LADDER[0], prevP = 1.0; for(int k = 0; k < BARRIER_LADDER_COUNT; k++) { double p = m_excOut.At(off + k); if(p <= want) { //--- Crossed between rung k-1 and k. Interpolate in the probability, not the multiple: the //--- ladder is geometric, so a linear read in p is the less distorted of the two. double span = prevP - p; double frac = (span > 1e-9) ? (prevP - want) / span : 0.0; return prev + frac * (BARRIER_LADDER[k] - prev); } prev = BARRIER_LADDER[k]; prevP = p; } //--- Never crossed: the horizon reaches past the top rung more often than tau allows, so the honest //--- answer is the top rung rather than an extrapolation off the end of the measured ladder. return BARRIER_LADDER[BARRIER_LADDER_COUNT - 1]; } //+------------------------------------------------------------------+ //| The verdict line. Skill = 1 - Brier(head)/Brier(base), the | //| standard Brier skill score: > 0 means the head beats the | //| constant base rate, 0 means it has learned exactly the base | //| rate, < 0 means it is worse than assuming nothing. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ExcursionReport(void) { if(CheckPointer(m_excNet) == POINTER_INVALID || m_excScored < EXCURSION_MIN_SCORED) return; //--- DECISION RUNGS, pre-registered as "the ones Stage 2 actually consumes", not chosen after //--- looking. double slMult, tpMult; BarrierMultiples(slMult, tpMult); bool decMask[]; DecisionRungMask(decMask); double headSum = 0.0, baseSum = 0.0, headDec = 0.0, baseDec = 0.0, headDj = 0.0, baseDj = 0.0; double trailDec = 0.0, headDecTrail = 0.0; double oracleDec = 0.0; string perRung = "", decList = ""; for(int k = 0; k < BARRIER_LADDER_COUNT; k++) { double hUp = m_excBrierHead[k], bUp = m_excBrierBase[k]; double hDn = m_excBrierHead[BARRIER_LADDER_COUNT + k], bDn = m_excBrierBase[BARRIER_LADDER_COUNT + k]; headSum += hUp + hDn; baseSum += bUp + bDn; double bTot = bUp + bDn; double sk = (bTot > 0.0) ? 100.0 * (1.0 - (hUp + hDn) / bTot) : 0.0; perRung += StringFormat(" %.2f:%+.1f%%", BARRIER_LADDER[k], sk); //--- Same mask the scorer accumulated its paired differences over, so the skill score and its //--- standard error describe the same rungs. if(!decMask[k]) continue; decList += StringFormat(" %.2f", BARRIER_LADDER[k]); headDec += hUp + hDn; baseDec += bUp + bDn; trailDec += m_excBrierTrail[k] + m_excBrierTrail[BARRIER_LADDER_COUNT + k]; headDecTrail += m_excBrierHeadT[k] + m_excBrierHeadT[BARRIER_LADDER_COUNT + k]; headDj += hUp + hDn; // same tally: every scored bar is a disjoint window baseDj += bUp + bDn; //--- Oracle constant for these rungs, closed form: for constant c over n bars with H positives, //--- Brier = n*c^2 - 2c*H + H, minimised at c = H/n giving H - H^2/n = H*(1 - H/n). for(int s = 0; s < 2; s++) { double H = (double)m_excOosHits[s * BARRIER_LADDER_COUNT + k]; double n = (double)m_excScoredD; if(n > 0.0) oracleDec += H * (1.0 - H / n); } } if(baseSum <= 0.0 || baseDec <= 0.0) return; double skill = 100.0 * (1.0 - headSum / baseSum); double skillDec = 100.0 * (1.0 - headDec / baseDec); double skillDj = (baseDj > 0.0) ? 100.0 * (1.0 - headDj / baseDj) : 0.0; //--- Against the BEST POSSIBLE CONSTANT on this very block. A head that only learned a level scores //--- positive against the frozen IS constant and <= 0 here, by construction. double skillOracle = (oracleDec > 0.0) ? 100.0 * (1.0 - headDec / oracleDec) : 0.0; //--- vs the TRAILING INCUMBENT, on an IDENTICAL bar set: m_excBrierHeadT accumulated the head's //--- Brier only on the bars the warm trailing window also scored (2026-08-11; this replaced //--- pro-rating headDec by coverage, which assumed head skill is uniform across the OOS walk //--- while the trail-scored subset systematically excludes each era's warm-up bars). double skillTrail = (trailDec > 0.0 && m_excTrailScored > 0) ? 100.0 * (1.0 - headDecTrail / trailDec) : -100.0; double monoPct = (m_excScored > 0) ? 100.0 * m_excMonoViol / m_excScored : 0.0; //--- ALL FOUR must hold. That threshold's shape - one number, no interval, no multiplicity //--- control, evaluated over a grid - is the shape of the four best-of-N traps already //--- documented in this project, and it would have passed Stage 2 on an artifact that the label //--- smoothing manufactured (see ExcursionTargets). bool passDec = (skillDec >= EXCURSION_SKILL_USEFUL_PCT); bool passOracle = (skillOracle >= EXCURSION_SKILL_USEFUL_PCT); //--- THE SKILL SCORES NOW CARRY A STANDARD ERROR, and the count thresholds they replace were //--- never a power calculation. Raising the split or shortening the horizon to clear it would be //--- fitting the experiment to the answer. double djMean = 0.0, djSe = 0.0, djT = 0.0; if(m_excScoredD > 1) { djMean = m_excDiffSum / m_excScoredD; double djVar = (m_excDiffSumSq / m_excScoredD) - (djMean * djMean); if(djVar < 0.0) djVar = 0.0; djSe = MathSqrt(djVar / m_excScoredD); djT = (djSe > 0.0) ? djMean / djSe : 0.0; } double trMean = 0.0, trSe = 0.0, trT = 0.0; if(m_excTrailScored > 1) { trMean = m_excTrailDiffSum / m_excTrailScored; double trVar = (m_excTrailDiffSumSq / m_excTrailScored) - (trMean * trMean); if(trVar < 0.0) trVar = 0.0; trSe = MathSqrt(trVar / m_excTrailScored); trT = (trSe > 0.0) ? trMean / trSe : 0.0; } //--- BOTH still required: the SE says the effect is real, EXCURSION_SKILL_USEFUL_PCT says it is big //--- enough to be worth replacing a constant that cannot fail. A tiny effect measured precisely is //--- still not worth a network. bool passDj = (skillDj >= EXCURSION_SKILL_USEFUL_PCT && m_excScoredD >= EXCURSION_MIN_DISJOINT_SANITY && djT >= EXCURSION_MIN_SIGMA); //--- CAN THIS CONFIGURATION EVER REACH EVEN THE SANITY FLOOR? Disjoint windows are scored bars over //--- the horizon, and the scored bars are the OOS slice, so the count has a CEILING no number of //--- eras moves. Says "not in this configuration" rather than "wait longer" - see ReportDetectability. int djSpacing = (int)MathMax(m_barrierHorizonBars, 1); int djCeiling = (m_excScored > 0) ? (int)(m_excScored / djSpacing) : 0; bool djUnreachable = (djCeiling < EXCURSION_MIN_DISJOINT_SANITY); //--- THE INCUMBENT TEST. A rolling rung frequency needs no model, no 760 inputs and no training; //--- if the head cannot beat it there is nothing here worth deploying a network for, however //--- well it beats a frozen constant. bool passTrail = (skillTrail >= EXCURSION_SKILL_USEFUL_PCT && m_excTrailScored >= EXCURSION_MIN_DISJOINT_SANITY && trT >= EXCURSION_MIN_SIGMA); bool passMono = (monoPct <= EXCURSION_MAX_MONO_VIOL_PCT); string verdict; if(passDec && passDj && passOracle && passMono && passTrail) verdict = " <-- PASSES ALL FOUR. Stage 2 is justified: drive SL/TP and sizing off" " ExcursionQuantile. Still RISK CONTROL ONLY - expectancy is -costs at zero directional" " edge whatever the stop distance, and under prop DD limits LOWER variance also lowers" " P(reach target before limit), so 'better drawdown' here is a choice about WHICH" " failure mode, not an improvement. Race it against a trailing-quantile incumbent" " before shipping."; else { verdict = " <-- NOT JUSTIFIED. Failing:"; if(!passDec) verdict += " [decision rungs]"; if(!passDj) verdict += (m_excScoredD < EXCURSION_MIN_DISJOINT_SANITY) ? (djUnreachable ? StringFormat(" [disjoint sample CANNOT REACH %d HERE - %d of a ceiling of %d," " being %d scored bars over a %d-bar horizon. More eras cannot" " raise it; only more OOS bars or a shorter horizon can]", EXCURSION_MIN_DISJOINT_SANITY, m_excScoredD, djCeiling, m_excScored, djSpacing) : " [disjoint sample too small]") : StringFormat(" [disjoint skill %+.1f%% at %.2f sigma - needs %+.1f%% AND %.1f" " sigma]", skillDj, djT, EXCURSION_SKILL_USEFUL_PCT, EXCURSION_MIN_SIGMA); if(!passOracle) verdict += " [beaten by the best constant on this block - level, not per-bar]"; if(!passMono) verdict += " [survival curve not monotone]"; if(!passTrail) verdict += (m_excTrailScored < EXCURSION_MIN_DISJOINT_SANITY) ? " [trailing incumbent not warm enough to race]" : StringFormat(" [vs trailing quantile %+.1f%% at %.2f sigma - needs %+.1f%% AND" " %.1f sigma; below that no net is needed]", skillTrail, trT, EXCURSION_SKILL_USEFUL_PCT, EXCURSION_MIN_SIGMA); verdict += ". Stage 2 must not be built on this."; } //--- THROTTLED (2026-08-19): a settled verdict (risk control, not edge - see project memory) //--- that printed ~780 chars every era per member. Cadence via TrainLogDue; VerboseMode = every era. if(TrainLogDue()) Print(ID + StringFormat(": excursion head - DECISION rungs%s (live geometry stop %.2f target %.2f):" " skill %+.1f%% vs IS constant, %+.1f%% at %.2f sigma on %d DISJOINT" " windows (every %d bars), %+.1f%% vs the BEST constant on this block," " %+.1f%% at %.2f sigma vs a TRAILING quantile on %d bars |" " non-monotone curves" " %.1f%% | all-rung aggregate %+.1f%% on %d bars (fitted on %d) | per-rung" " ATR:skill%s |%s", decList, slMult, tpMult, skillDec, skillDj, djT, m_excScoredD, (int)MathMax(m_barrierHorizonBars, 1), skillOracle, skillTrail, trT, (int)m_excTrailScored, monoPct, skill, m_excScored, m_excBaseTotal, perRung, verdict)); } //+------------------------------------------------------------------+