Warrior_EA/Expert/Training/PooledGate.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

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