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