Warrior_EA/Expert/Training/OosTally.mqh
AnimateDread 8f2164698b feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.

DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
  FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
  in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
  adoption, exit-policy replay, excursion MI targets, the drift verdict
  (DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
  the barrier defines, the .cfg geometry adopt (slots kept as zeros for
  the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
  re-keys the meta head onto label agreement (descriptor loses its two
  geometry slots).

REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
  FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
  never cached, so it can never freeze as a false Neutral; training,
  calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
  directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
  lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
  resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
  m_erasSinceBest, ensemble vote outcome arrays -> label arrays.

STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.

Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.

Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00

134 lines
8.3 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//| One era's out-of-sample confusion counts, and the rates they |
//| imply. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_TRAINING_OOSTALLY_MQH
#define WARRIOR_TRAINING_OOSTALLY_MQH
//+------------------------------------------------------------------+
//| ONE ERA'S CONFUSION COUNTERS, WITH ONE LIFETIME. |
//| |
//| Every field here is cleared at the start of an era and read at |
//| the end of it. They were twenty-one separate members cleared |
//| one-per-line at a single site, which is the arrangement that |
//| produced 7452bd1: a second reset path cleared ONE of ten tallies |
//| and nine stale numerators went on being divided by restarted |
//| denominators. A partial reset is not expressible here. |
//| |
//| GROUPED BY LIFETIME, NOT BY NAME. m_oosSamples and dOosError look |
//| like they belong in here and do not: both are RUN-level (a |
//| rolling EMA and its sample count, reset only when the weights |
//| are), and the status panel prints them together on purpose. Two |
//| quantities that are cleared at different times must not sit in |
//| one object, however similar their names - that similarity is |
//| precisely what makes the mistake easy to make later. |
//| |
//| The derived rates live here too, because every one of them was |
//| written out at the call site and the "-1 means not measurable" |
//| convention was re-spelled at each: `(bars > 0) ? (int)MathRound( |
//| 100.0 * x / bars) : -1` appeared a dozen times. One rounding rule |
//| and one absent-value sentinel, so a reader checking whether the |
//| gate and the log agree does not have to compare a dozen |
//| expressions character by character. |
//+------------------------------------------------------------------+
struct SOosTally
{
//--- BY TRUE CLASS: how many OOS bars actually were each class, and how many the net caught.
//--- hits/total is per-class RECALL - the convergence gate that stops a model "winning" by
//--- calling everything Neutral.
int buyTotal, sellTotal, neutralTotal;
int buyHits, sellHits, neutralHits;
//--- BY PREDICTED CLASS: the same population keyed the other way, so hits/predicted is per-class
//--- PRECISION - of the bars it called Sell, how many were Sell - rather than recall.
int buyPredicted, sellPredicted, neutralPredicted;
int buyPredictedHits, sellPredictedHits, neutralPredictedHits;
//--- FIRED: the same era under the LIVE decision rule - a directional call counted whenever the
//--- prior-corrected posterior is non-Neutral, i.e. exactly the bars on which the deployed EA
//--- would cast a directional vote. This is the population the deploy gate judges.
int buyFired, sellFired;
int buyFiredHits, sellFiredHits;
//--- Summed claimed confidence over this era's scored bars, for the calibration blend.
double confidenceSum;
SOosTally(void) { Reset(); }
//--- ALL of them, together. The only way to clear this object.
void Reset(void)
{
buyTotal = sellTotal = neutralTotal = 0;
buyHits = sellHits = neutralHits = 0;
buyPredicted = sellPredicted = neutralPredicted = 0;
buyPredictedHits = sellPredictedHits = neutralPredictedHits = 0;
buyFired = sellFired = 0;
buyFiredHits = sellFiredHits = 0;
confidenceSum = 0.0;
}
//--- THE DENOMINATORS, derived once ---------------------------------------------------------
//--- Every OOS bar this era gets exactly one true class, so these three sum to the era's bars.
int Bars(void) const { return buyTotal + sellTotal + neutralTotal; }
//--- The bars the deployed EA would have TRADED, and how many of those paid.
int DirCalls(void) const { return buyFired + sellFired; }
int DirHits(void) const { return buyFiredHits + sellFiredHits; }
//--- The bars that actually WERE directional - the base rate coverage is judged against.
int DirTrue(void) const { return buyTotal + sellTotal; }
//--- Correct calls across all three classes, for the calibration blend's accuracy term.
int Hits(void) const { return buyHits + sellHits + neutralHits; }
//--- THE RATES ------------------------------------------------------------------------------
//--- -1 IS "NOT MEASURABLE", never 0. A gate reading 0 would treat an unmeasured quantity as a
//--- failure; every caller here tests `< 0` to mean "this does not block".
static int Pct(const int num, const int den)
{
return (den > 0) ? (int)MathRound(100.0 * num / den) : -1;
}
static double PctD(const double num, const double den)
{
return (den > 0.0) ? 100.0 * num / den : -1.0;
}
//--- Share of this era's OOS bars. The one denominator every predicted/true/fired rate uses, so
//--- the log's columns are directly comparable rather than nearly so.
int PctOfBars(const int count) const { return Pct(count, Bars()); }
//--- Per-class recall, blocked below a minimum true-sample count: a class with almost no true
//--- bars this era must not gate convergence, because there is nothing to measure it against.
//--- (Requiring a real minimum is what stopped a run converging at era 44 on an OOS window
//--- holding ZERO true Buy/Sell bars.)
int BuyRecallPct(const int minSamples) const
{ return (buyTotal >= minSamples) ? Pct(buyHits, buyTotal) : -1; }
int SellRecallPct(const int minSamples) const
{ return (sellTotal >= minSamples) ? Pct(sellHits, sellTotal) : -1; }
int NeutralRecallPct(const int minSamples) const
{ return (neutralTotal >= minSamples) ? Pct(neutralHits, neutralTotal) : -1; }
//--- Neutral's predicted and fired shares are the RESIDUAL, not their own counters: every OOS bar
//--- gets exactly one call, so what is not Buy and not Sell is Neutral by construction.
int NeutralPredictedShare(void) const
{ return PctOfBars(Bars() - buyPredicted - sellPredicted); }
int NeutralFiredShare(void) const
{ return PctOfBars(Bars() - buyFired - sellFired); }
//--- THE DEPLOY-GATE INPUTS -----------------------------------------------------------------
//--- Coverage is measurable only when there were bars AND some of them were directional; without
//--- both, "the model calls too rarely" and "there was nothing to call" are indistinguishable.
bool CoverageMeasurable(void) const { return (Bars() > 0 && DirTrue() > 0); }
double CoveragePct(void) const
{ return CoverageMeasurable() ? PctD(DirCalls(), Bars()) : -1.0; }
double BaseRatePct(void) const
{ return CoverageMeasurable() ? PctD(DirTrue(), Bars()) : -1.0; }
double DirPrecPct(void) const { return PctD(DirHits(), DirCalls()); }
//--- CHANCE PRECISION: what an always-call-one-direction model would have scored on these same
//--- bars - the base rate of the LARGER directional class, because beating the smaller one proves
//--- only that the model found the drift.
double ChancePrecPct(void) const
{
return CoverageMeasurable()
? PctD((double)MathMax(buyTotal, sellTotal), (double)Bars()) : -1.0;
}
//--- An era in which the model fired nothing directional at all. Distinguished from a bad era on
//--- purpose: there is no precision to rank it by, so it must not win "best era".
bool FullyCollapsed(void) const { return (DirCalls() <= 0); }
};
#endif // WARRIOR_TRAINING_OOSTALLY_MQH
//+------------------------------------------------------------------+