Warrior_EA/Expert/AIBase/Excursion.mqh

736 lines
37 KiB
MQL5
Raw Permalink Normal View History

feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//| Excursion.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//| EXCURSION-SIZE HEAD - a SECOND, small network that predicts HOW |
//| FAR price travels, never WHICH WAY. |
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Build the head's topology: input window -> one hidden dense -> 2 |
//| x ladder sigmoid outputs. |
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//+------------------------------------------------------------------+
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.
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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);
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
ArrayInitialize(m_excBrierHeadT, 0.0);
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
ArrayInitialize(m_excOosHits, 0);
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
//--- 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;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
m_excBaseTotal = 0;
m_excScored = 0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
m_excScoredD = 0;
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
m_excDiffSum = 0.0;
m_excDiffSumSq = 0.0;
m_excTrailDiffSum = 0.0;
m_excTrailDiffSumSq = 0.0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
m_excMonoViol = 0;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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;
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
if(!m_ladder.Has(idx))
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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:
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
m_excTgt.Clear();
for(int k = 0; k < BARRIER_LADDER_COUNT; k++)
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
m_excTgt.Add(m_ladder.UpAge(idx, k) > 0 ? 1.0 : 0.0);
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
for(int k = 0; k < BARRIER_LADDER_COUNT; k++)
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
m_excTgt.Add(m_ladder.DownAge(idx, k) > 0 ? 1.0 : 0.0);
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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.
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
m_excTrainTick++;
if((m_excTrainTick % EXCURSION_TRAIN_STRIDE) != 0)
return;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
if(!ExcursionTargets(idx))
return;
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
ulong excT0 = GetMicrosecondCount();
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
if(!m_excNet.feedForward(TempData))
return;
//--- Base rates accumulated from the SAME rows the head trains on - IS only.
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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);
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
//--- 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;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
}
//+------------------------------------------------------------------+
//| 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. |
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//+------------------------------------------------------------------+
void CExpertSignalAIBase::ExcursionScoreStep(int idx)
{
if(CheckPointer(m_excNet) == POINTER_INVALID || m_excBaseTotal <= 0)
return;
if(!ExcursionTargets(idx))
return;
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
//--- DISJOINT WINDOWS ONLY - both the honest statistic AND the whole scoring cost.
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
int hz = (int)MathMax(m_barrierHorizonBars, 1);
bool disjoint = ((m_excScored % hz) == 0);
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
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.
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
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;
}
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
//--- 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;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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;
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
//--- 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.
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
if(y > 0.5)
m_excOosHits[k]++;
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
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;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
//--- 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;
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
double brTrail = (tr - y) * (tr - y);
m_excBrierTrail[k] += brTrail;
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
//--- 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.
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
m_excBrierHeadT[k] += brHead;
if(isDec)
barTrailDiff += brTrail - brHead;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
}
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
}
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
//--- 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;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
if(m_excTrailN >= EXCURSION_TRAIL_MIN_N)
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
{
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
m_excTrailScored++;
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
m_excTrailDiffSum += barTrailDiff;
m_excTrailDiffSumSq += barTrailDiff * barTrailDiff;
}
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
ExcursionTrailPush();
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
m_excScoredD++; // every bar reaching here IS a disjoint one now
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
m_excScored++;
}
//+------------------------------------------------------------------+
//| Advance the trailing-climatology ring by one bar. |
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
//+------------------------------------------------------------------+
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. |
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
//+------------------------------------------------------------------+
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);
}
}
//+------------------------------------------------------------------+
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//| 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;
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
m_excBrierHeadT[k] = 0.0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
m_excOosHits[k] = 0;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
m_excBrierTrail[k] = 0.0;
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
m_excTrailHits[k] = 0;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
}
m_excScored = 0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
m_excScoredD = 0;
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
m_excDiffSum = 0.0;
m_excDiffSumSq = 0.0;
m_excTrailDiffSum = 0.0;
m_excTrailDiffSumSq = 0.0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
m_excMonoViol = 0;
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
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").
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
if(ArraySize(m_excTrailRing) > 0)
ArrayInitialize(m_excTrailRing, 0);
m_excTrailHead = 0;
m_excTrailCount = 0;
m_excTrailN = 0;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
m_excTrailScored = 0;
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
}
//+------------------------------------------------------------------+
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
//| 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;
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
//--- 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;
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
slRung = CFirstPassageLadder::RungFor(adverse);
tpRung = CFirstPassageLadder::RungFor(favour);
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
//--- 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;
}
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
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)
{
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
//--- See SGeometryScan::startTick. The clock starts on the first ATTEMPT, not the first success - a bar the
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
//--- head cannot answer for still costs a forward pass. After the budget this simply stops
//--- contributing, leaving the exit replay it rides on untouched.
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
if(m_geo.startTick == 0)
m_geo.startTick = GetTickCount();
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
else
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
if(GetTickCount() - m_geo.startTick >= GEOMETRY_BUDGET_MS)
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
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);
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
int incSl = CFirstPassageLadder::RungFor(slMult - m_spreadAtr);
int incTp = CFirstPassageLadder::RungFor(tpMult + m_spreadAtr);
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
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;
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
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.
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
if(!m_ladder.OutcomeR(barIdx, isLong, incSl, incTp, m_spreadAtr, rInc, incTo))
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
return;
refactor(barriers): the ladder is an object, and its snap rule is one rule CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages plus the terminal travel) and every question asked of them. The signal keeps one member where it kept three arrays and a lifespan scalar. WHAT THIS ENDS. The log-space rung snap existed THREE times: once as LadderRungFor, twice written out inline inside LadderWinShare - and LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a rung chosen here and a rung chosen there are the same rung". A comment asking a reader to keep three copies equal by hand is the arrangement CMetaFamilies was built to end. It is now one static RungFor(), so the two rungs agree by construction. The bounds test was spelled out at four sites and the "0 means never, tie goes to the stop" comparison at three. Now Has() and FirstTouch(), once. The four-site bounds test was also subtly weak: it computed `idx * COUNT` and tested only the upper end, so a negative index slipped through into a negative array read. Row() rejects it. Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR multiples; what a spread costs and how long the walk ran are facts the caller supplies. Every answer is now a function of its inputs alone - which is the point, because this is the barrier arithmetic that failed its own acceptance test in b5e22a1 and it has never been runnable without a chart, a net and a broker attached. BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against its predecessor with the rename map reversed; the only differences are the substitutions named above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
if(!m_ladder.OutcomeR(barIdx, isLong, candSl, candTp, m_spreadAtr, rCand, candTo))
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
return;
if(incTo)
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
m_geo.incOpen++;
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
if(candTo)
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
m_geo.candOpen++;
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
double d = rCand - rInc;
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
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++;
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
}
//+------------------------------------------------------------------+
//| 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)
{
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
if(m_geo.trades < 2 || !TrainLogDue())
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
return;
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
double mean = m_geo.diffSum / m_geo.trades;
double var = (m_geo.diffSumSq / m_geo.trades) - (mean * mean);
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
if(var < 0.0)
var = 0.0;
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
double effN = EffectiveSampleSize((double)m_geo.trades);
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
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"
fix(geometry): a free zero made "never resolve" the winning geometry The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate geometry beats the global pair on every SP500 member at 2-3 sigma. It does not. It said so because a bar that reached neither barrier scored 0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a losing baseline a free zero is a win, so the widest candidate always came out ahead - and the reported gain ordered itself by timeout share, not by skill: PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL) HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma) CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL) LSTM 27.1% -> +0.158 R (head +1.67 sigma) Monotone in the timeout share and inverted against the sigma gate. The acceptance test written when this was built - "the sigma gate predicts LSTM helps and CONV hurts; if the R difference does not reproduce that ordering, something is wrong" - is what caught it. A trade that reaches neither barrier is not worth zero. It is closed at the horizon, which is what the scheduled close-all does live and what SimulateTradeOutcome's timeout path already charges. So mark it there: TripleBarrierLabel now publishes the signed close-to-close travel at the last bar it actually visited (m_termTravelCache, same validity flag as the excursion and ladder caches), and LadderOutcomeR prices a timeout off it instead of returning false. A bar that cannot be evaluated under BOTH pairs is now dropped whole - scoring one leg and defaulting the other is the same bug in a smaller costume. Second defect, same function: CandidateGeometryFor applied neither of the floors the global derivation applies, so on USDJPY it chose stop 2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy floor. c3daded in miniature: a selector optimising its own criterion with no reference to the decision criterion. Both floors now apply, and the ratio is re-checked AFTER the per-leg rung snap, which can lose it. Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 - 41% of the ensemble's capable weight and the loudest voice on the chart, off two effective observations. It also lifted the computed vote ceiling to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never printed on a chart whose peak vote is 14 and whose practical ceiling without that member is 18.8. The pooled rate is now shrunk toward the coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls, and the tiers shrink toward the shrunk value rather than the raw one. A member with ~300 effective calls moves by ~0.4pp; the 19-fire member goes 0.37 -> ~0.15. MEASUREMENT ONLY still - no order reads any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
" 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"
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
" order uses this. Below 2 sigma the one global pair is doing as well, and it costs no"
" forward pass.",
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
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,
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
mean, t, effN,
fix(geometry): a shutdown abort cleared one tally of ten Found by grouping, not by looking for it. The candidate-geometry scan kept ten accumulators as ten separate members. Era start cleared all ten in a ten-line block. The shutdown-abort path inside the exit-policy simulation cleared m_geoTrades and nothing else, so nine partial sums - diffSum, diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen, startTick - survived the abort with the aborted era's values. The next era then accumulated onto those sums while counting from zero, so the paired mean is sum/trades with a numerator carrying an extra era's worth of difference. The paired sigma is worse: diffSumSq inherits the same contamination, so the scan reports a tighter or wider spread than it measured depending on what the abort happened to be holding. That is the SAME arithmetic that failed its own acceptance test in b5e22a1, where the reported gain turned out to be monotone in timeout share. This is not that bug - it needs a shutdown mid-era to fire - but it lands on the same number, and any geometry reading taken from a session that was stopped and restarted is suspect. SGeometryScan now owns all ten with one Reset(). Both sites call it. A partial reset is no longer something that can be written: there is one door, and it clears everything behind it. The struct initialises itself, so the ten constructor-initialiser entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields there, which is a second reason ten loose members was the wrong shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
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
feat(geometry): measure per-candidate barriers against the one global pair Stage 2a of the candidate-conditional geometry the record has named as next and never built. MEASUREMENT ONLY - no order uses it yet. WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed geometry, and its verdict stands: real skill, 0 operating points clearing break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so the lever is the geometry, not the veto. A per-candidate rung means a per-candidate break-even, which a binary gate cannot express. HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent pair AND under the pair this bar's excursion head would choose, and the paired difference is accumulated in R with a 2-sigma test. Both legs come from the SAME first-passage ladder - four array reads, no re-walk, exact even on the ~28% of bars where both barriers were touched. Mixing the ladder with the price walk here would measure the discrepancy between two of our own evaluators rather than the effect of the geometry, which is precisely what f8ac10c had to unpick one layer over. The candidate pair applies the GLOBAL derivation's own rule per bar: stop at BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable. Neither creates expectancy; what moves is the break-even, which is why the report quotes R and never a win rate. FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R difference reproduces that ordering across members, the head's usefulness is confirmed by a second, independent measurement. If it does not, something is wrong and this must not be wired to orders. Two things caught while writing it, both silent if missed: - The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is risk = ladder + spread but reward = ladder - spread, so the two legs convert with OPPOSITE signs. The stop leg had the sign backwards. - A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and a head forward per OOS call to a walk that already runs unchunked at era end on a single-threaded EA. That is the shape that got the process force- terminated on 2026-08-21. It stops scoring, never the replay, and the report prints how many calls it covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
? 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. |
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//+------------------------------------------------------------------+
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. |
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
//+------------------------------------------------------------------+
void CExpertSignalAIBase::ExcursionReport(void)
{
if(CheckPointer(m_excNet) == POINTER_INVALID || m_excScored < EXCURSION_MIN_SCORED)
return;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
//--- DECISION RUNGS, pre-registered as "the ones Stage 2 actually consumes", not chosen after
//--- looking.
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
double slMult, tpMult;
BarrierMultiples(slMult, tpMult);
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
bool decMask[];
DecisionRungMask(decMask);
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
double headSum = 0.0, baseSum = 0.0, headDec = 0.0, baseDec = 0.0, headDj = 0.0, baseDj = 0.0;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
double trailDec = 0.0, headDecTrail = 0.0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
double oracleDec = 0.0;
string perRung = "", decList = "";
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
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);
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
//--- Same mask the scorer accumulated its paired differences over, so the skill score and its
//--- standard error describe the same rungs.
if(!decMask[k])
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
continue;
decList += StringFormat(" %.2f", BARRIER_LADDER[k]);
headDec += hUp + hDn;
baseDec += bUp + bDn;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
trailDec += m_excBrierTrail[k] + m_excBrierTrail[BARRIER_LADDER_COUNT + k];
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
headDecTrail += m_excBrierHeadT[k] + m_excBrierHeadT[BARRIER_LADDER_COUNT + k];
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
headDj += hUp + hDn; // same tally: every scored bar is a disjoint window
baseDj += bUp + bDn;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
//--- 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];
perf: the excursion head cost 3.6x era time - cut its dispatches ~250x Measured on exc-race-v3: LSTM era 300s -> 1087s (net 272->748s, "other" 30->337s). My estimate had been "single-digit percent". The cost is per-DISPATCH, not per-FLOP, and therefore hits EVERY backend: the head is 19k weights and ~2.4 GFLOP an era - seconds of arithmetic - but ~48k forward/backward calls x several layer submits each, and its 760-wide layer exceeds the CPU DLL's inline threshold so each one pays a real handoff. The classifier's own net time tripled too, from contention with a second pool on an already-full box. Three changes, all backend-neutral because they remove submits rather than tune threads: SCORE ONLY DISJOINT WINDOWS (~64x). Adjacent bars share all but one bar of their horizon, so 16k consecutive bars were always ~250 independent observations - the full-sample tally was never worth more than the disjoint one, it just quoted an n that was ~64x too large. Dropping it costs nothing statistically and removes 63 of every 64 forward passes. The two parallel tallies collapse into one, which is also less code. The trailing ring still advances on every bar: it needs the outcome SEQUENCE, and that is array lookups, not a forward pass. TRAIN ON EVERY 4th PRIMARY BAR (4x). The target is low-dimensional and strongly autocorrelated - neighbouring bars carry near-identical excursion information - so per-bar training buys resolution the target does not have. Strided on ATTEMPTS, not acceptances, so a stretch of unlabelled bars cannot silently change the spacing. OWN TIMING COLUMN. The head's passes were landing in the era line's "other" bucket, which is how a 3.6x regression read as an unexplained jump in the one column nobody attributes. A cost that cannot be seen in the timing line cannot be traded off against anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:38:29 -04:00
double n = (double)m_excScoredD;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
if(n > 0.0)
oracleDec += H * (1.0 - H / n);
}
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
}
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
if(baseSum <= 0.0 || baseDec <= 0.0)
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
return;
double skill = 100.0 * (1.0 - headSum / baseSum);
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
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;
fix: the trailing incumbent read the future across eras; cold AD blocks cached zeros as truth Three findings from the 2026-08-11 audit: 1. The excursion head's trailing-quantile ring was deliberately never cleared between eras ("a rolling estimate of the market, not of the era") - but pass 3 re-walks the SAME OOS window every era, so at each walk's restart the ring still held the outcome masks of the newest OOS bars from the previous walk: the chronological FUTURE of the bars about to be scored. For the first ~window+horizon pushes of every era the "trailing" incumbent was partly a leading one - conservative for the gate (an informed incumbent is a harder hurdle) but exactly the self-made-artifact class 06d4785 hunts. The ring now clears at era-score reset; the warm-up bars simply don't score the trail race, which the m_excTrailN gating already accounts for. 2. skillTrail compared the head's FULL-block Brier (pro-rated by coverage) against the incumbent's subset sum - valid only if head skill is uniform across the OOS walk, while the trail-scored subset systematically excludes each era's warm-up bars. The audit also found m_excBrierHeadD/BaseD/ m_excOosHitsD declared, zeroed and never accumulated (dead since e2c9593 made every scored bar disjoint). The dead trio is replaced by m_excBrierHeadT: the head's Brier accumulated only on the bars the warm incumbent also scored, so the race now compares both predictors on an identical bar set. 3. The AD/Wyckoff feature blocks read GetData with no EMPTY_VALUE guard; a cold (still-calculating) indicator returns EMPTY_VALUE everywhere, the sanitize loop rewrote that to 0.0, and the bar SUCCEEDED - so BufferTempData cached an all-zero Wyckoff block as a success for the whole bar frame: the one path the f6150ee only-cache-successes rule cannot see, because it never fails (the ba13eef class, arriving through values that never fail; a resumed model's era-0 prebuild starts milliseconds after OnInit). ADIndicatorCold() probes the NEWEST bar - EMPTY_VALUE there means async warm-up (transient reject, retried), while deep bars beyond the buffered depth keep the sanitize loop's neutral-fill so degraded history still trains. Also fixed m_featureCacheValid's declaration comment, which still described the pre-f6150ee cached-miss semantics. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:20:24 -04:00
//--- 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)
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
? 100.0 * (1.0 - headDecTrail / trailDec) : -100.0;
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
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).
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
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.
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
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.
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
bool passTrail = (skillTrail >= EXCURSION_SKILL_USEFUL_PCT
&& m_excTrailScored >= EXCURSION_MIN_DISJOINT_SANITY
&& trT >= EXCURSION_MIN_SIGMA);
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
bool passMono = (monoPct <= EXCURSION_MAX_MONO_VIOL_PCT);
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
string verdict;
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
if(passDec && passDj && passOracle && passMono && passTrail)
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
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.";
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
else
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
{
verdict = " <-- NOT JUSTIFIED. Failing:";
if(!passDec)
verdict += " [decision rungs]";
if(!passDj)
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
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);
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
if(!passOracle)
verdict += " [beaten by the best constant on this block - level, not per-bar]";
if(!passMono)
verdict += " [survival curve not monotone]";
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
if(!passTrail)
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
verdict += (m_excTrailScored < EXCURSION_MIN_DISJOINT_SANITY)
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
? " [trailing incumbent not warm enough to race]"
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
: 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);
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
verdict += ". Stage 2 must not be built on this.";
}
feat(logs): throttle the settled per-era diagnostics - measured 22MB/9.5h of confirmed-working systems Measured from the journal (2026-08-19): the era deep-dive line (~2KB) plus the excursion verdict, tier re-rank, calibration move, barrier hold and selection-regressed note each printed EVERY era for EVERY member - ~940 eras/member/day - long after the systems they watch were confirmed working. Yesterday's file was 1.3GB (70% of it the news-filter calendar spam the sweep fix already removed). VerboseMode returns as an INPUT (demoted 2026-08-01 for the marketplace; that track is dead since the 2026-08-16 pivot) and gains a second job: false throttles each settled per-era print to eras 0-3 plus every TRAIN_LOG_EVERY_ERAS-th (25 ~= one deep-dive per ~15min per member); true restores the per-era firehose, flippable live. Never throttled: anything that marks a CHANGE - new bests, restores + eta decays, plateau stage transitions, deploy approvals, warnings, errors, the label-cache/adoption one-shots, and the combined-vote gate line (the active system's primary telemetry, still every era). Semantic fixes over blanket gating: - barrier hold now ARMS silently and prints only when the hold outlasts the 2-min report interval - a brief hold every era is the design, the long hold is the watchdog case the line exists for; - the ensemble deploy REFUSAL prints immediately when its reason changes (that is a finding), on cadence when unchanged; - the filtered-view census prints when its RESULT moves (drawn count, or strongest vote by >=2pp) and at least every 10th sweep. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:36:15 -04:00
//--- 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):"
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
" 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"
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
" %.1f%% | all-rung aggregate %+.1f%% on %d bars (fitted on %d) | per-rung"
" ATR:skill%s |%s",
fix(excursion): the sample gate asked for more windows than the configuration can contain EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so 200 was unreachable and every era printed "[disjoint sample too small]", which an operator reads as "wait longer". Unpassable by construction - the identical failure this file already documents one gate down, one layer up. The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars, so it failed the same 200 for the same reason, at 130. Raising OOSSplit or shortening the horizon would clear it and would be fitting the experiment to the answer. Instead, ask the question the count was standing in for - is the skill bigger than its own noise: - The scorer banks one paired Brier difference per DISJOINT window over the decision rungs (base-head, and trail-head). Disjoint by construction, so no EffectiveSampleSize deflation applies - striding by the horizon is what buys that - and paired on identical bars, so the correlation between the two predictors cancels instead of needing to be estimated. - passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND >= 2 sigma, with the count reduced to a sanity floor of 30. - Both sigmas print on the verdict line. This is not a lowered bar. The 2% skill requirement, the oracle control and the monotone test are untouched, and the sigma test can fail where the count test never spoke: if +8.5% is noise across 147 windows, it will now say so. DecisionRungMask() is the single definition of "rung the decision depends on", called by both the scorer and the report, so the standard error is computed over exactly the rungs the skill score is. The report's inline copy of the bracketing test is gone. Also prints whether the disjoint count is BELOW ITS CEILING or at it, so "not enough yet" and "not in this configuration" stop reading the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
decList, slMult, tpMult, skillDec, skillDj, djT, m_excScoredD,
(int)MathMax(m_barrierHorizonBars, 1), skillOracle, skillTrail, trT,
feat: race the excursion head against a trailing-quantile incumbent Beating a frozen global constant is the weakest admissible bar for replacing a global constant. The honest incumbent is a rolling rung frequency: it adapts to the volatility regime - exactly what the head claims to predict - and needs no model, no 760 inputs and no training. Implemented as a ring of per-bar outcome bitmasks (32 rungs fit one ulong), sized horizon + EXCURSION_TRAIL_WINDOW. The newest `horizon` entries are held back UNRESOLVED: a bar's rung outcomes are only known one horizon later, so using them would be lookahead and would flatter the incumbent into an opponent the head could never fairly beat. Pass 3 walks oldest-to-newest, so "pushed more than horizon bars ago" is exactly "resolved by now". Each push is O(rungs), not O(window). The head's decision-rung Brier is pro-rated to the trailing estimate's coverage before the ratio, since the incumbent only scores bars where its window is warm. This line is worth reading on its own, independently of the head: if the trailing quantile beats the global constant, that is a cheap risk-control win available with no machine learning at all - and it is the same number either way, so the run answers both questions in one pass. The ring is deliberately NOT reset per era - it estimates the market, not the era, and re-warming 500 bars every era would leave the incumbent unusable over the first chunk of every scoring pass, handing the head a free win on exactly those bars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:57:11 -04:00
(int)m_excTrailScored, monoPct, skill,
fix: the excursion gate would have passed Stage 2 on an artifact I made Second-opinion review killed the +4.2% far-rung result, correctly, and the mechanism is my own bug. A head trained toward {0.05,0.9} converges to 0.05+0.85p, so its bias is 0.05-0.15p: negative where p is near 1, POSITIVE where p < 1/3, growing monotonically as the rung gets farther. Against a baseline frozen at the IS rate, an upward-biased head scores positive Brier skill whenever the OOS rate merely sits above the IS rate. Predicted signature: huge negatives near, ~zero at p=1/3, growing positives far. Observed: -82% ... -0.6% ... +1.2/+2.7/+4.2. The far rungs were not the clean end of a distorted measurement, they were the other face of the same artifact. Everything before 25aca83 is void. The gate was a bare `skill >= 2%` point estimate over 8 rungs x 4 topologies x N eras, reported per era - a best-of-~300 with no interval and no multiplicity control, which is the shape of the four traps already documented here. It now needs FOUR things at once: DECISION RUNGS only the rungs ExcursionQuantile actually reads at the live geometry (target 1.62, stop 3.31 ATR), fixed before looking. Skill at 5 ATR is skill about a distance no order is placed at - and the TARGET side currently interpolates 1.5/2.0, which measured -2.2% and -1.3%. DISJOINT SAMPLE one bar per horizon. Adjacent bars share 63 of 64 horizon bars, so ~16k scored bars is ~250 independent ones and every SE over the full set is ~8x understated. VS ORACLE the best constant achievable ON THE SCORED BLOCK, closed form from H and n (Brier = H*(1-H/n)). A head that learned only a LEVEL nearer the OOS rate than the frozen IS constant scores positive against the old baseline and <= 0 here. This is the control that separates per-bar skill from base-rate drift. MONOTONE CURVE P(reach k) must be non-increasing in k. Nothing constrained 8 independent sigmoids to obey that, and ExcursionQuantile returns the FIRST crossing - so a tangled curve is misread exactly where the head is least sure. Counted and reported, not silently used. The pass message now also states what a pass would and would not buy: 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" is a choice of failure mode, not a win. Still owed before any Stage 2: a race against a trailing-quantile incumbent and a vol-feature logistic. Beating a frozen global constant is the weakest admissible bar for replacing a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:49:32 -04:00
m_excScored, m_excBaseTotal, perRung, verdict));
feat: excursion-size head (Stage 1, measurement only) Direction is closed - normalised asymmetry fails on three instruments with a working positive control, and the classifier's own best-of-999 era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is a different question and RANGE clears at ~4x its null. Checked the denomination before building on that, since the source memo warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is predictable" is a claim about travel RELATIVE to current ATR, not a restatement of "ATR is autocorrelated". It is exactly the part a fixed multiple (stop 3.31*ATR, target 1.64*ATR) discards. A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches ladder rung k) upward and downward. Survival parameterisation rather than regressing the multiple, because it needs nothing new from CNet: sigmoid outputs and the per-neuron delta the `total != 3` branch already applies (a quantile head would need a linear activation and a pinball gradient in Network.mqh, Network.cl and the DirectML path, on a class four topologies share). Targets are free - m_ladderUpAt already records first-touch age per rung with 0 meaning never reached. Separate net, not extra outputs on the classifier: more outputs would change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push the count off 3 - the exact condition backProp uses to select the joint softmax gradient the 3-class head depends on. The classifier is bit-for-bit unaffected and this is removable without trace. STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the constant per-rung base rate - the baseline a fixed ATR multiple already assumes - with both predictors fitted IS and evaluated OOS, so neither gets a look at the test set. Positive skill justifies Stage 2 (drive SL/TP and sizing off ExcursionQuantile, which is defined and deliberately uncalled). Zero or negative means ATR already carries everything and Stage 2 must not be built. Trains only on primary occurrences: the replay queue oversamples for CLASS balance, and a direction-balanced sample is a biased SIZE sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:40:01 -04:00
}
//+------------------------------------------------------------------+