forked from animatedread/Warrior_EA
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>
248 lines
13 KiB
MQL5
248 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| PooledGate.mqh |
|
|
//| Cross-instrument certification for the deploy decision |
|
|
//| Measured 2026-08-17 on SP500 H4: the OOS window is 4,738 bars |
|
|
//| and the mean triple-barrier label lifespan is 75.6 bars, so the |
|
|
//| holdout carries ~63 INDEPENDENT observations (see |
|
|
//| EffectiveSampleSize). Certifying a 3pp edge at 2 sigma needs |
|
|
//| ~1,036. The deploy gate was therefore unreachable by arithmetic |
|
|
//| - not because the models were short, but because the window |
|
|
//| cannot resolve the question either way. Sixteen times short. |
|
|
//+------------------------------------------------------------------+
|
|
#property strict
|
|
#ifndef WARRIOR_TRAINING_POOLEDGATE_MQH
|
|
#define WARRIOR_TRAINING_POOLEDGATE_MQH
|
|
|
|
//--- Records older than this are ignored. A chart that was stopped days ago is not evidence about the
|
|
//--- model running now, and a stale pool silently certifying a dead configuration is the failure mode
|
|
//--- this guards. Long enough to survive an overnight run, short enough that yesterday cannot vote.
|
|
#define POOL_MAX_AGE_HOURS 12
|
|
//--- Minimum distinct instruments before the pooled statistic is allowed to gate anything. Below this
|
|
//--- the pool is not a pool - it is one symbol with extra steps, and SE_CORR would equal SE_INDEP.
|
|
#define POOL_MIN_INSTRUMENTS 3
|
|
//--- Ceiling on peers read, sizing the fixed arrays. Well above the 24-instrument catalog.
|
|
#define POOL_MAX_RECORDS 64
|
|
//--- Bumped whenever the record layout changes. A reader that finds a different version SKIPS the
|
|
//--- record rather than misreading its columns - the stale-enum trap in a different costume.
|
|
//--- v2: the targetRR column went with the barrier geometry - the swing label has no structural
|
|
//--- break-even, so poolability is timeframe + version alone.
|
|
#define POOL_RECORD_VERSION 2
|
|
#define POOL_DIR "Warrior_EA\\Pool"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| One instrument's contribution to the pooled certificate. |
|
|
//+------------------------------------------------------------------+
|
|
struct SPoolRecord
|
|
{
|
|
string symbol;
|
|
int timeframe;
|
|
double chancePct; // this symbol's own zero-skill rate
|
|
double winPct; // what the model actually collected
|
|
double effN; // INDEPENDENT calls, already deflated by label lifespan
|
|
double lifespanBars;
|
|
long eraCount;
|
|
datetime stamp;
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| CROSS-INSTRUMENT CERTIFICATION. |
|
|
//| |
|
|
//| One symbol's holdout cannot resolve the deploy question - see the |
|
|
//| file header: sixteen times short. The answer is more instruments, |
|
|
//| not more eras, so each chart publishes its own record and every |
|
|
//| chart reads the pool. |
|
|
//| |
|
|
//| It owns a DIRECTORY OF CSV FILES and nothing else. It does not |
|
|
//| know what a model is, and the two things it needs - the symbol's |
|
|
//| own numbers and the ratio they were measured at - arrive as |
|
|
//| arguments. That is why it takes no data view: a gate over files |
|
|
//| is not a reader of training data. |
|
|
//+------------------------------------------------------------------+
|
|
class CPooledGate
|
|
{
|
|
private:
|
|
//--- ONE-SHOT warning latch. A pool that cannot be written is a silent loss of the whole
|
|
//--- mechanism, and a mechanism that declines to act must announce it - but once, not per era.
|
|
bool m_writeWarned;
|
|
|
|
int ReadPooledEvidence(double &pooledExcessPp, double &seIndep, double &seCorr,
|
|
string &detail);
|
|
public:
|
|
CPooledGate(void) : m_writeWarned(false) { }
|
|
//--- `id` is passed rather than bound so there is no init-order question about when the identity
|
|
//--- became available.
|
|
void Publish(const string id, const SPoolRecord &rec);
|
|
//--- The pooled verdict across every peer on this timeframe.
|
|
bool Passes(string &report);
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
//| Publish this instance's evidence. Called once per era, after the |
|
|
//| local gate has computed its own numbers. |
|
|
//| |
|
|
//| Writes ONLY this instrument's file. The filename carries symbol |
|
|
//| and timeframe so two charts can never collide, and a chart that |
|
|
//| restarts overwrites its own record rather than accumulating a |
|
|
//| history that would let one instrument vote many times. |
|
|
//+------------------------------------------------------------------+
|
|
void CPooledGate::Publish(const string id, const SPoolRecord &rec)
|
|
{
|
|
//--- Nothing measurable to contribute. Writing a placeholder would let a symbol with no evidence
|
|
//--- dilute the pool's weighting, which is the opposite of what inverse-variance weighting is for.
|
|
if(rec.chancePct <= 0.0 || rec.chancePct >= 100.0 || rec.winPct < 0.0 || rec.effN < 2.0)
|
|
return;
|
|
//--- _Period, not Period(): inside a CExpertBase subclass the bare call resolves to the inherited
|
|
//--- SETTER bool CExpertBase::Period(ENUM_TIMEFRAMES) rather than the builtin. Same predefined
|
|
//--- variable AutoTune.mqh uses to build its own per-symbol/timeframe filename.
|
|
string fn = StringFormat("%s\\%s_%d.csv", POOL_DIR, rec.symbol, rec.timeframe);
|
|
int h = FileOpen(fn, FILE_COMMON | FILE_WRITE | FILE_CSV | FILE_ANSI |
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE, ',');
|
|
if(h == INVALID_HANDLE)
|
|
{
|
|
//--- One-shot: a pool that cannot be written is a silent loss of the whole mechanism, and a
|
|
//--- mechanism that declines to act must announce it (the third quiet no-op this codebase has
|
|
//--- been bitten by).
|
|
if(!m_writeWarned)
|
|
{
|
|
m_writeWarned = true;
|
|
Print(id + StringFormat(": WARNING - cannot write the pooled-gate record %s (error %d). This "
|
|
"instrument will not contribute to cross-instrument certification and "
|
|
"the pool will be short one member.", fn, GetLastError()));
|
|
}
|
|
return;
|
|
}
|
|
FileWrite(h, POOL_RECORD_VERSION, rec.symbol, rec.timeframe,
|
|
rec.chancePct, rec.winPct, rec.effN, rec.lifespanBars, rec.eraCount, (long)TimeCurrent());
|
|
FileClose(h);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Read every peer's record and combine. Returns the number of |
|
|
//| instruments that qualified; the pooled figures come back through |
|
|
//| the out-params. |
|
|
//| |
|
|
//| INCLUDES THIS INSTRUMENT: its own file was just written, so the |
|
|
//| directory scan picks it up like any other and there is no special |
|
|
//| case to get wrong. |
|
|
//+------------------------------------------------------------------+
|
|
int CPooledGate::ReadPooledEvidence(double &pooledExcessPp, double &seIndep, double &seCorr,
|
|
string &detail)
|
|
{
|
|
pooledExcessPp = 0.0;
|
|
seIndep = 0.0;
|
|
seCorr = 0.0;
|
|
detail = "";
|
|
SPoolRecord rec[POOL_MAX_RECORDS];
|
|
int count = 0;
|
|
string found;
|
|
long searchHandle = FileFindFirst(POOL_DIR + "\\*.csv", found, FILE_COMMON);
|
|
if(searchHandle == INVALID_HANDLE)
|
|
return 0;
|
|
do
|
|
{
|
|
if(count >= POOL_MAX_RECORDS)
|
|
break;
|
|
int h = FileOpen(POOL_DIR + "\\" + found, FILE_COMMON | FILE_READ | FILE_CSV | FILE_ANSI |
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE, ',');
|
|
if(h == INVALID_HANDLE)
|
|
continue;
|
|
int ver = (int)FileReadNumber(h);
|
|
//--- Version mismatch: SKIP, never reinterpret. Reading v2 columns as v1 would produce a
|
|
//--- plausible-looking record built from the wrong fields, which is worse than one fewer member.
|
|
if(ver != POOL_RECORD_VERSION)
|
|
{
|
|
FileClose(h);
|
|
continue;
|
|
}
|
|
SPoolRecord r;
|
|
r.symbol = FileReadString(h);
|
|
r.timeframe = (int)FileReadNumber(h);
|
|
r.chancePct = FileReadNumber(h);
|
|
r.winPct = FileReadNumber(h);
|
|
r.effN = FileReadNumber(h);
|
|
r.lifespanBars = FileReadNumber(h);
|
|
r.eraCount = (long)FileReadNumber(h);
|
|
r.stamp = (datetime)(long)FileReadNumber(h);
|
|
FileClose(h);
|
|
//--- POOLABILITY. Symbol deliberately does NOT have to match - that is the entire point - but
|
|
//--- everything that changes what the numbers MEAN does.
|
|
if(r.timeframe != (int)_Period)
|
|
continue; // different bar semantics
|
|
if(r.effN < 2.0 || r.chancePct <= 0.0 || r.chancePct >= 100.0)
|
|
continue; // no usable estimate
|
|
if((TimeCurrent() - r.stamp) > POOL_MAX_AGE_HOURS * 3600)
|
|
continue; // stale: a stopped chart is not evidence about now
|
|
rec[count++] = r;
|
|
}
|
|
while(FileFindNext(searchHandle, found));
|
|
FileFindClose(searchHandle);
|
|
if(count <= 0)
|
|
return 0;
|
|
//--- FIXED-EFFECTS COMBINATION. Each symbol is scored against ITS OWN chance rate, so symbols with
|
|
//--- different geometries and different drifts are directly comparable in this one currency.
|
|
double sumInvVar = 0.0;
|
|
for(int i = 0; i < count; i++)
|
|
{
|
|
double var = BinomialVar(rec[i].chancePct / 100.0, rec[i].effN); // in fraction^2
|
|
if(var <= 0.0)
|
|
continue;
|
|
sumInvVar += 1.0 / var;
|
|
}
|
|
if(sumInvVar <= 0.0)
|
|
return 0;
|
|
double weighted = 0.0, sumWSd = 0.0;
|
|
for(int i = 0; i < count; i++)
|
|
{
|
|
double var = BinomialVar(rec[i].chancePct / 100.0, rec[i].effN);
|
|
if(var <= 0.0)
|
|
continue;
|
|
double w = (1.0 / var) / sumInvVar;
|
|
weighted += w * (rec[i].winPct - rec[i].chancePct); // percentage points
|
|
//--- Perfectly-correlated bound: the weighted sum of the individual standard deviations, which
|
|
//--- is what the pooled SE degenerates to when every member moves together.
|
|
sumWSd += w * MathSqrt(var) * 100.0;
|
|
detail += StringFormat("%s%s %.0f%%/%.0f%% n%.0f w%.2f", (detail == "" ? "" : " "),
|
|
rec[i].symbol, rec[i].winPct, rec[i].chancePct, rec[i].effN, w);
|
|
}
|
|
pooledExcessPp = weighted;
|
|
seIndep = 100.0 * MathSqrt(1.0 / sumInvVar);
|
|
seCorr = sumWSd;
|
|
return count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| The cross-instrument verdict, as one log line and one boolean. |
|
|
//| |
|
|
//| Gates on the PESSIMISTIC bound. A pooled edge that clears SE_CORR |
|
|
//| cannot be explained by the members being correlated, because that |
|
|
//| bound already assumes they are perfectly correlated. |
|
|
//+------------------------------------------------------------------+
|
|
bool CPooledGate::Passes(string &report)
|
|
{
|
|
report = "";
|
|
double excess = 0.0, seIndep = 0.0, seCorr = 0.0;
|
|
string detail = "";
|
|
int members = ReadPooledEvidence(excess, seIndep, seCorr, detail);
|
|
if(members < POOL_MIN_INSTRUMENTS)
|
|
{
|
|
report = StringFormat(" | POOL %d/%d instruments - not enough to certify across symbols yet"
|
|
" (run more charts on this timeframe; each one is worth far more"
|
|
" independent evidence than more bars of the same symbol)",
|
|
members, POOL_MIN_INSTRUMENTS);
|
|
return false;
|
|
}
|
|
double bar = EDGE_MIN_SIGMAS * seCorr;
|
|
bool pass = (excess > bar);
|
|
//--- The credit the pool WOULD earn if its members were independent. Printed because the gap between
|
|
//--- the two bounds is the whole cost of refusing to assume independence, and it is the number that
|
|
//--- says whether adding a genuinely uncorrelated instrument is worth more than another correlated one.
|
|
double credit = (seIndep > 0.0) ? seCorr / seIndep : 1.0;
|
|
report = StringFormat(" | POOLED CERTIFICATE across %d instruments: excess %+.2fpp vs a %.2fpp bar"
|
|
" (%.0f x SE %.2fpp, the ALL-CORRELATED bound; SE would be %.2fpp if the"
|
|
" instruments were independent, a %.1fx diversification credit this gate"
|
|
" deliberately declines to claim) -> %s [%s]",
|
|
members, excess, bar, EDGE_MIN_SIGMAS, seCorr, seIndep, credit,
|
|
(pass ? "PASSES" : "fails"), detail);
|
|
return pass;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
#endif // WARRIOR_TRAINING_POOLEDGATE_MQH
|