//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //| Is this era's model good enough to trade, and how good was it? | //+------------------------------------------------------------------+ #ifndef WARRIOR_TRAINING_DEPLOYGATE_MQH #define WARRIOR_TRAINING_DEPLOYGATE_MQH //+------------------------------------------------------------------+ //| THE DEPLOY DECISION - ONE ARITHMETIC, TWO POPULATIONS. | //| | //| A member's own OOS calls and the ensemble's combined vote are | //| judged by the same rule: clear a coverage floor, beat chance by | //| EDGE_MIN_SIGMAS standard errors, be two-sided, and rank by | //| precision discounted for under-coverage. That rule was written | //| out twice, and the second copy carried three comments asking a | //| reader to keep it in step with the first by hand - "same intent | //| as the member gate's coverage floor", "the two gates have to | //| apply the identical correction or the ensemble becomes the easier | //| one to clear", "same lexicographic ordering as isBetterEra". | //| | //| They had already fallen out of step once: 2c443ba found the | //| ensemble gate certifying a vote the EA never casts, in the wrong | //| CURRENCY and against the wrong DENOMINATOR. A rule maintained in | //| two places by comment is a rule that certifies two different | //| things eventually. | //| | //| So EvaluateRates() is the arithmetic, and the two places where | //| the populations GENUINELY differ are arguments rather than | //| branches: | //| | //| chancePct - the zero-skill reference. The member gate uses the | //| larger of always-long/always-short on its bars. The ensemble | //| gate first filters by the direction policy, because with | //| shorts blocked "always short" is not a strategy anyone could | //| run and ranking a long-only book against it would score it | //| against a baseline the policy forbids. | //| twoSided - the anti-degenerate test. The member gate reads | //| per-side RECALL against DEPLOY_MIN_SIDE_RECALL_PCT; the | //| ensemble reads whether the vote actually fired both ways. | //| Same intent, different measurables. | //| | //| THE BAR IS CHANCE PLUS SIGMAS x SE, never a fixed win rate. A | //| model that reproduces the directional drift has found the drift, | //| not an edge. Observed 2026-08-01 against a fixed bar: a | //| perceptron deployed at edge +0pp. | //| | //| effN IS AN ARGUMENT, deliberately. Overlapping labels are worth | //| less than their count, only the model knows its own overlap, and | //| passing the deflated n in keeps that policy where it is measured. | //| It also makes a live inconsistency visible rather than buried: | //| the two FAMILY-WISE selection gates still compute their SE from | //| RAW n, the most permissive standard error in the codebase. That | //| is recorded here and deliberately NOT changed - tightening those | //| gates is a policy call, not a refactor. | //| | //| TRADEABILITY IS ALSO THE RANKING KEY, on purpose. It feeds | //| isBetterEra and the learning-rate decay, so an under-covering or | //| one-sided era must not become best-so-far in the first place - | //| checking it only at deploy time would let a run spend its whole | //| era budget improving on an era it could never ship. | //| | //| Reads no chart, holds no net, prints nothing, opens no file: the | //| decision behind every deployment can be exercised against a | //| made-up tally instead of only by running a chart and reading a | //| log an hour later. | //+------------------------------------------------------------------+ //--- One formatter, so no site can print the score in the wrong unit. string DeployScoreText(const double score) { return StringFormat("%.1f%%", score); } struct SDeployVerdict { //--- WHAT THE ERA MEASURED. bool measurable; // false = "nothing to judge", never "judged and failed" double coveragePct; // share of scored bars this population would have traded double baseRatePct; // share that actually were directional double minCoveragePct; // the floor coverage must clear double precPct; // label-agreement precision of the calls it did make double chancePct; // what the best always-call-one-direction book scores //--- THE BAR. double precSE; // SE of the CHANCE rate over the INDEPENDENT calls double edgeFloorPct; // chance + EDGE_MIN_SIGMAS x SE. The number to beat. double effN; // independent observations behind it //--- THE VERDICT. bool twoSided; bool tradeable; bool degenerate; // fired nothing at all - no precision to rank by double coverageCredit; // 1.0 at or above the floor, pro-rata below it double selectionScore; // THE ranking key, discounted by that credit //--- SE OF selectionScore, in the score's own win-rate points. The plateau ladder needs to know //--- how much of an era-to-era move is noise. double scoreSE; SDeployVerdict(void) { Reset(); } void Reset(void) { measurable = twoSided = tradeable = false; degenerate = true; coveragePct = baseRatePct = minCoveragePct = -1.0; precPct = chancePct = -1.0; precSE = edgeFloorPct = effN = 0.0; coverageCredit = 1.0; selectionScore = 0.0; scoreSE = 0.0; } //--- THE ARITHMETIC. `calls` are the bars this population would have traded, `bars` the bars it //--- was scored over, `dirTrueBars` how many were genuinely directional. `effNCalls` is the //--- INDEPENDENT-observation count behind `calls` (see EffectiveSampleSize) - never the raw //--- count, or overlapping labels certify an edge that is not there. void EvaluateRates(const int calls, const int bars, const int dirTrueBars, const double precPctIn, const double chancePctIn, const double effNCalls, const bool twoSidedIn) { Reset(); //--- Not measurable without BOTH scored bars and some directional truth among them: without //--- the second, "it calls too rarely" and "there was nothing to call" read identically. measurable = (bars > 0 && dirTrueBars > 0); precPct = precPctIn; chancePct = chancePctIn; twoSided = twoSidedIn; degenerate = (calls <= 0); coveragePct = measurable ? 100.0 * calls / bars : -1.0; baseRatePct = measurable ? 100.0 * dirTrueBars / bars : -1.0; minCoveragePct = measurable ? baseRatePct * MIN_COVERAGE_FRACTION_OF_BASE_RATE : -1.0; //--- SE of the CHANCE rate, not of the model's own: the question is how far a no-information //--- book could stray by luck over this many INDEPENDENT calls. effN = effNCalls; precSE = (calls > 0) ? BinomialSEPct(chancePct / 100.0, effN) : 0.0; //--- EXACT one-sided binomial floor (System\BinomialStats.mqh), not the normal approximation //--- chance+sigmas*SE: at this gate's effN (typically tens of independent calls) the normal //--- approximation is anticonservative - no continuity correction, understates the tail - so //--- the PASS/FAIL bar below is now exact. precSE/scoreSE stay the normal approximation: they //--- only rank eras against each other (coverageCredit, selectionScore), where a monotone //--- estimate is enough and changing it would be pure churn on a heuristic, not a correctness //--- fix on a gate. //--- calls<=0 -> chancePct itself (no evidence, no floor above chance), matching precSE's own //--- 0.0 in that branch above - NOT 100.0/"impossible". BarUnreachable() reads edgeFloorPct //--- >= 100 as "no precision could ever clear this bar", which is a claim about the GEOMETRY, //--- not about an era that simply had nothing to score yet. edgeFloorPct = (calls > 0) ? ExactEdgeFloorPct(chancePct, effN, EDGE_MIN_SIGMAS) : chancePct; //--- Ranking: DISCOUNTED by how far short of the coverage floor the era fell. Undiscounted, a //--- single lucky call scores 100% and nothing can ever beat it, so the checkpoint freezes on //--- one sample and the run burns to the era cap. coverageCredit = 1.0; if(minCoveragePct > 0.0 && coveragePct >= 0.0) coverageCredit = MathMin(1.0, coveragePct / minCoveragePct); //--- THE OBJECTIVE: per-class precision must clear its chance rate by EDGE_MIN_SIGMAS standard //--- errors. Coverage and two-sidedness gate it too: a precision computed over a handful of //--- calls, or over one live side, is not one anyone can trade. tradeable = measurable && precPct >= 0.0 && twoSided && coveragePct >= minCoveragePct && (precPct > edgeFloorPct); selectionScore = (precPct >= 0.0) ? precPct * coverageCredit : 0.0; scoreSE = precSE * coverageCredit; } //--- ONE MEMBER'S OWN ERA. Its zero-skill reference is the base rate of the larger directional //--- class on these bars, and its two-sidedness is read from per-class RECALL: -1 there means //--- "too few true bars of that class to judge", which must pass rather than read as a dead side. void Evaluate(const SOosTally &tally, const double effNCalls, const int buyRecallPct, const int sellRecallPct) { bool bothLive = (buyRecallPct < 0 || buyRecallPct >= DEPLOY_MIN_SIDE_RECALL_PCT) && (sellRecallPct < 0 || sellRecallPct >= DEPLOY_MIN_SIDE_RECALL_PCT); EvaluateRates(tally.DirCalls(), tally.Bars(), tally.DirTrue(), tally.DirPrecPct(), tally.ChancePrecPct(), effNCalls, bothLive); } //--- Is the deploy bar even reachable? Above 100% it is not "hard", it is impossible, and no //--- amount of training moves it - only more independent observations do. bool BarUnreachable(void) const { return (edgeFloorPct >= 100.0); } //--- How far the calls sat above chance, in percentage points. Negative means the model is //--- behind simply always calling one direction. double EdgePp(void) const { return (precPct >= 0.0 && chancePct >= 0.0) ? precPct - chancePct : 0.0; } }; #endif // WARRIOR_TRAINING_DEPLOYGATE_MQH //+------------------------------------------------------------------+