forked from animatedread/Warrior_EA
183 lines
11 KiB
MQL5
183 lines
11 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|
|
//+------------------------------------------------------------------+
|