//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+