Commit graph Warrior_EA/Expert/Training
Author SHA1 Message Date
AnimateDread
2e22e714c6 fix(pool): length-prefix the fingerprint - the cross-instrument pool was inert
STrainPoolHeader wrote its fingerprint into a FILE_BIN stream as
FileWriteString(h, fingerprint + "\n") and read it back with
FileReadString(h) - no length argument. In binary mode FileWriteString
emits the characters raw: no length prefix, no terminator, and "\n" is
just another character rather than a delimiter anything honours. The
reader had nothing to stop at, over-read into the float rows that follow,
and returned the fingerprint plus a few bytes of binary garbage - so
`fingerprint != wantFp` could never succeed between two genuinely
identical models.

Verified in the bytes rather than inferred: xxd on a v1 file shows three
ints then the fingerprint starting immediately at offset 12 with no count
in front of it, and EURUSD/USDJPY/USDCAD all stored width 624 with
byte-identical fingerprints while each one's log rejected the other two as
"different model fingerprint". The StringReplace on "\n" is the tell that
a delimiter was intended.

Cross-asset-class peers really are incompatible and always will be - FX
majors carry XA:6, indices/metals/oil carry XA:6:IDX2, giving widths
600/612/624 - which is why the reject list looked plausible and this went
unread. The three FX majors were always poolable and never pooled.

Length-prefixes the string, bounds-checks the count before sizing a read
from it, and bumps TRAINPOOL_RECORD_VERSION 1 -> 2 so existing files are
refused by the version gate with a reason instead of being misread.

Also documents, without changing, why Signal_ThresholdOpen is now a
unanimity rule: the vote is a weighted mean of tier weights, those fell
from ~70 to ~30 with the pivot-event label, so PCT_25 went from ~36% of
the reachable ceiling to ~83%. Measured: all 6 symbols clear their
precision bar, 4 of 6 fail only on coverage, and coverage decays 6.8% ->
2.2% over 35 eras as the models specialise - which shrinks effN and so
RAISES the deploy bar at flat precision. PCT_20 (a 3-of-4 quorum) is the
indicated change but is left unmade: MT5 stores input values per chart in
profiles\Charts\*\chart*.chr, so an already-attached EA ignores this
default entirely - confirmed by a full close/recompile/relaunch cycle
after which the log still read "fired at vote>=25%".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:41:41 -04:00
AnimateDread
0fddaeea12 fix: correct edge floor percentage calculation and logging for model training 2026-08-25 23:16:05 -04:00
AnimateDread
b2784b5a4d Enhance Feature and Topology Interfaces with Bulk Operations and Cache Management
- Added bulk read/write methods for feature caches in IFeaturesView and its implementations to optimize performance.
- Introduced LabelCacheInvalidateAll method to manage label cache invalidation alongside feature cache.
- Implemented PooledIndependentBars method in topology interfaces to account for additional independent observations.
- Enhanced risk budget management with throttling for peak-equity updates to reduce unnecessary file operations.
- Improved error handling and logging for ATR trailing stops to ensure better visibility of issues.
- Updated alt-data handling to prevent unnecessary operations during testing and optimization phases.
2026-08-25 22:51:50 -04:00
AnimateDread
1baa13c5b4 refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.

RETRAIN-NEUTRAL, and that is the property that made this safe:

  - The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
    if/else. Every direction model already took the SWG1 arm, so
    collapsing it to an unconditional append is byte-identical. No .nnw or
    .cfg is orphaned or re-keyed.
  - NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
    returned 0 for every direction model, so the input layer is unchanged.
  - DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
    off AND meta on - a config that never shipped. Every existing .db keeps
    its filename.

Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.

Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.

Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.

EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.

Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
AnimateDread
ec1692f348 feat(mi): the screen is an alarm, not a gate
The MI suite kept its one irreplaceable job - the label-alignment
lookahead scan, whose margin is priced by the headline permutation
null and whose validity is proven by the positive control. Everything
that judged or vetoed on top of that measurement is gone:

- m_dirEvidence deploy veto deleted from all four deploy sites. The
  policy is that screens are priors, not gates; the family-wise
  selection test on held-out precision is the deploy protection, and
  a marginal per-bar MI test cannot veto a model that reads the
  window jointly (the report itself said so on every print).
- Per-column CFeatureSelector deleted; BlockPermuteLabels (the null
  engine ScoreMiSample depends on, ragged-tail fix intact) moves to
  AutoTune.mqh as a free function.
- Feature-lag profile deleted, with its MI_LAG_* constants and
  BuildMiSample's featureBarOffset; MiShiftPad no longer pads by
  m_historyBars.

Compile: 0 errors, 0 warnings (stage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:01:08 -04:00
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
AnimateDread
2abca1298c fix(labels): a closed candle shifts the cache, it does not invalidate it
Series indices are relative to now, so one new bar moves every cached bar's
index by one. EnsureBarCachesCapacity answered that by wiping the label cache,
the excursion caches, the ladder and the feature cache and rebuilding the whole
prebuild from scratch - on any timeframe where a bar closes before a run
finishes, the labels were being recomputed continuously and the training set
never held still.

The labels do not change when a candle closes. ShiftBarCaches moves every
per-bar cache up by the number of new bars, marks only those newest bars as
unfilled, and leaves the rest exactly as computed. CFirstPassageLadder gets a
matching Shift (resizing directly rather than through Allocate, which zeroes the
ages this is preserving).

Refuses, falling back to the full rebuild, when a prebuild is mid-flight: its
cursor is an index into the array being moved.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 18:36:16 -04:00
AnimateDread
0cd20a5749 diag(geometry): the sweep must not present "least negative" as a recommendation
First 35 eras across both charts, this run:

  shipped 1.21/2.43 (SP500) and 1.26/2.52 (USDJPY): mean -0.0525R,
      positive in 6 of 35 eras
  best plateau after the neighbourhood guard: mean +0.0292R,
      positive in only 17 of 35
  most-recommended pair: 20.00/0.50, seven times - a ~40:1 lottery that is
      simply the least negative cell in an all-negative grid

The recommendation jumps between opposite corners of the ladder between
consecutive eras, which is a grid fitting noise rather than a geometry worth
adopting. Two changes so the line cannot be misread:

- GEOSWEEP_MAX_TIMEOUT_SHARE (0.70): a cell where most trades never touch
  EITHER barrier is not a geometry being tested, it is the horizon close being
  measured. 20.00/20.00 timed out on 100% of trades and was still selected.
  Excluded from SELECTION only; the cell stays filled and readable.
- When the winning plateau is <= 0 the line now says so in those words:
  "NOTHING ON THE LADDER PAYS ... the pair below is the LEAST NEGATIVE cell,
  not an edge."

Still measurement only - nothing reads the recommendation and no geometry moves.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:59:49 -04:00
AnimateDread
1882f87451 feat(geometry): price every stop/target pair on the trades the model actually called
Step 1 of decoupling SL/TP from training. The geometry is currently chosen
BEFORE the model exists - excursions -> stop at a quantile -> target at the
policy minimum ratio -> labels -> the net learns those labels - so it has never
been asked which pair maximises expectancy GIVEN WHAT THE MODEL CAN PREDICT.
The scan meant to answer that reports "0 ELIGIBLE candidates" on this config
(every rung disqualified by the close-all clamp), so nothing has ever compared
the shipped pair to an alternative.

This needs no retrain and no backtest. CFirstPassageLadder already stores the
first-touch AGE of every rung on both sides and OutcomeR() resolves ANY pair
exactly with the spread charged the way the fill charges it - so 14x14 pairs
over one era's OOS calls is a few thousand array reads.

- Expert/Training/GeometrySweep.mqh: CGeometrySweep accumulates (n, sumR,
  sumR^2, timeouts) per rung pair from the model's own directional OOS calls.
  Reads no chart, holds no net, opens no file - exercisable against a
  hand-built ladder, same doctrine as SDeployVerdict.
- Best() ranks on the 3x3 NEIGHBOURHOOD mean, not the cell itself. A 14x14 grid
  read at its single highest cell is a best-of-196 maximum, biased upward by
  construction - the same selection problem the deploy gate corrects across
  eras. A pair whose neighbours also pay is a plateau; a lone spike is a lucky
  run of trades and does not survive the next window. GEOSWEEP_MIN_TRADES (30)
  keeps thin cells out of the selection entirely.
- Wired into pass 3 where the call and the bar index are both in hand, reset per
  era, reported at pass-3 completion beside ReportCandidateGeometry. ONE line,
  and only when the recommendation CHANGES - it prints the shipped pair's
  expectancy and the best pair's on the SAME trades, so "better" is a difference
  rather than two numbers from two populations.

Measurement only: nothing reads the recommendation yet and no geometry moves.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:35:12 -04:00
AnimateDread
d12b742a40 fix(deploy): print the selection score in the unit it is actually in
selectionScore used to be a win rate in percentage points and printed at one
decimal everywhere. Under DeployOnExpectancy it is expected value in R, so
"%.1f" rendered every real score as "0.0" - era 2's +0.05R and a genuine zero
looked identical, which makes the journal useless for watching the ranking the
plateau ladder is doing.

One formatter, DeployScoreText(), next to the score it formats: "%.3fR" under
expectancy, "%.1f%%" under significance. Routed all nine print sites through it
(ensemble era line, best-so-far, panel, regression, new-best, era-cap prompts,
the convergence line, the deploy dialog) and dropped the "%" suffixes they had
hardcoded. No new prints, no new log lines.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:21:57 -04:00
AnimateDread
9883b209c7 feat(deploy): ship on positive EXPECTANCY, and let the chart draw before convergence
TWO CHANGES, both of which turn a permanent "nothing happens" into a decision.

1. THE DEPLOY GATE ASKS THE WRONG QUESTION. tradeable required the win rate to
   clear chance by EDGE_MIN_SIGMAS - "can I PROVE an edge exists" from one OOS
   window. On H4 that asks ~66% against a market supplying ~53%, so it is
   unreachable by construction and no run has ever deployed through it.

   SDeployVerdict now also carries the economics of the geometry actually being
   traded - cost-adjusted break-even and reward:risk, both from the new
   CostAdjustedGeometry() so a spread convention cannot be applied to one and
   missed on the other - and derives

       E[R] = (p - p*) * (1 + RR)

   which is exactly zero at break-even by construction, so "profitable" and
   "beats break-even" can never disagree. Under DeployOnExpectancy (new input,
   default ON) tradeable becomes E[R] > 0 and selectionScore ranks eras by
   expectancy instead of precision. Coverage and both-sides-live still gate
   both: an expectancy over a handful of one-sided calls is not tradeable.

   The struct also publishes scoreSE - the SE of selectionScore IN THE SCORE'S
   OWN UNITS - because the score changes units with the objective (win-rate
   points vs R). Both plateau bands now read it instead of precSE, which was
   right for one objective and dimensionally wrong for the other.

   Setting DeployOnExpectancy=false restores the previous behaviour exactly.

2. THE FILTERED VIEW COULD NOT DRAW WHILE ANY MODEL WAS TRAINING.
   HistoricalNetVote built its divisor from VoteCapableWeight(), which answers
   "may this member move real money" and returns 0.0 for an AI member until the
   whole run converges. So the reconstruction's divisor was zero on EVERY bar,
   every bar was skipped as "nobody looked", and the chart drew nothing at all -
   for the entire training run, which before the plateau noise band was forever.
   Reported as "no signals drawn since the refactor".

   New ReconstructionWeight(): the same weight WITHOUT the converged-run
   requirement, overridden on the AI member to ModuleWeight() gated on
   SelfRanked() only. The overlay is a picture of what the vote WOULD have
   shown, which a mid-training model can answer - the chart HUD already says so
   with its "(trn)" marker. Live Direction() still uses VoteCapableWeight(), so
   no untrained model gains a say in an order.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:15:06 -04:00
AnimateDread
9c31625aae fix(training-pool): say why a peer was rejected instead of adopting nothing in silence
Two charts (SP500 H4 + USDJPY H4) ran with the pool enabled and produced no
TrainPool directory, no adopted rows and not one journal line. The pool was
inert and there was no way to tell that from "the feature is off".

It could never have fired: the fingerprint is not symbol-invariant. It hashes
NeuronsCount, which counts the alt-data columns - and those are per-symbol
(SP500 carries cot_spec_net, the FX majors cot_idx_1y/3y/chg_4w) - and the
cross-asset block appends ":IDX2" when base currency == profit currency, true
of an index and false of a pair. SP500 came out 50 features wide under
XA:6:IDX2, USDJPY 52 wide under XA:6. Compatible() gates on both, so adoption
was zero by construction.

- STrainPoolHeader::MismatchReason() replaces the bare Compatible() predicate
  and names the mismatch; Compatible() now delegates to it, so "may I adopt"
  and "why not" can never drift apart.
- CTrainPoolReader::Adopt() reports its own verdict - adopted, alone, or every
  peer rejected with the reason per file - and reports it on CHANGE only. An
  era over a warm feature cache runs in a fraction of a second here, so a
  per-era line would bury the journal. The duplicate Print in RunPass2 is gone;
  pool state is now reported from exactly one place.
- CTrainPoolWriter::Publish() rate-limits to TRAINPOOL_MIN_PUBLISH_SEC (300s).
  Every era re-derives the same rows from the same in-sample span, so per-era
  publishing rewrote a multi-megabyte file continuously for no new information.
  The first publish is never delayed.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 14:43:12 -04:00
AnimateDread
e7b4442999 feat(training): TrainingPool - cross-instrument training rows, compile-verified
PooledGate pools the DECISION; this pools the DATA. Measured in research/edge.py with both arms
sharing calendar folds, exit-time purge, benchmark and scoring so training breadth is the only
variable: H4 k=2 gap +2.02pp at t_mkt 3.97, which CLEARS the Sidak bar of 3.69 over five feature
sets at df=6, replicated independently at D1 k=1 (+2.03pp, t_mkt 2.79). The per-instrument arm
was NEGATIVE on every feature set at both timeframes - it loses to "always take the drift side".
This EA trains one net per chart, which is that arm.

Rows, not symbols. Pointing the feature stack at another symbol needs per-symbol indicator
handles and this project has been bitten there twice - the handle leak that never released the
old handle, and the twelve "dead" handles that were one shared refcounted iMA. Each chart
instead computes its own features with its own handles and shares the NUMBERS. Sound only
because FeatureBuilder already ATR-normalises every price-unit feature, for exactly this reason
("instead of feeding e.g. 0.0005 on EURUSD").

Not a fingerprint participant: pooling changes what the model is trained ON, not what it IS, so
adding it would re-key every .nnw to record something outside the model's identity. The
fingerprint instead GATES adoption - it is the assertion that column k means the same thing in
both files - alongside a width check (a fingerprint match with a width mismatch means one side
pinned an older layout) and an exit-TIME purge, since a bar index cannot be compared across
instruments that each have their own calendar.

Writer and reader are separate classes: different reasons to change, different lifecycles, and
one class would carry the export buffers through every read. The file layout lives in one
STrainPoolHeader used by both sides so a layout change cannot be applied to the writer and
missed in the reader. Staging goes through System\AtomicFile rather than a second hand-rolled
temp-and-rename.

Compile-verified in isolation: 0 errors, 0 warnings. Staging junctions and harness removed; the
deployed .ex5 was never touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:46:58 -04:00
AnimateDread
523d4ab6d0 refactor(mi): dedupe CMetaCorpus's two MqlDateTime-from-fields blocks
LoadLargestOnDisk() and LoadFromConfigDb() each built an MqlDateTime
struct field-by-field then called StructToTime() to get an Add()
timestamp. Added a shared static BuildStamp(y,mo,d,h,mi); both loops
now call it. Pure relocation, no arithmetic/ordering change.
2026-08-24 03:13:31 -04:00
AnimateDread
7dbdac8ece feat(mi): per-column feature screen, and fix the block permutation it rides on
CFeatureSelector keeps the per-column MI vector ScoreMiSample has always
computed and thrown away. It is fed from inside the 200 draws
ReportFeatureLabelInformation already performs, so the screen costs an array
copy per draw and not one extra mutual-information computation.

The keep-mask is cut on the single-step maxT (Westfall-Young) statistic - a
column must beat the MAXIMUM of a null draw over all columns, which is strong
family-wise control needing no Bonferroni factor, and is the same null of the
maximum the headline verdict already trusts. The uncorrected per-comparison
p is reported alongside it; the gap between the two counts IS the multiplicity
correction, shown rather than described. Checked offline at 40 columns: 0/40
noise runs keep anything, where the uncorrected rule hands back ~2 columns per
run, and a planted column is recovered 40/40.

REPORT-ONLY. Nothing reads the mask. Pruning changes m_neuronsCount, which is
in BuildModelFingerprint(), which invalidates every .nnw - that is a retrain
across every chart and an operator's call to make after reading the report.

Also fixes the block permutation, found while moving it. When blockRows did
not divide n the short last block, drawn to a non-final slot, read past the
end of the array; the read was clamped to labels[n-1], duplicating one label
and truncating whichever block landed last. 18 of the 24 possible block orders
on n=10/blockRows=3 altered the class counts. A duplicated label concentrates
the class distribution, lowering H(Y) and so the null MI those draws can reach,
so p-values leaned toward significance - the permissive direction, and
m_dirEvidence is a deploy gate. Each block now contributes exactly its own
length. The invariance the old comment asserted ("a permutation preserves the
class counts - that invariance is itself a check on the shuffle") was never
actually compared anywhere; BlockPermute now checks it and returns false, and
all six shuffled call sites already guard on a negative return.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 18:45:00 -04:00
AnimateDread
e886139c81 refactor(meta): one corpus, three sources - not two corpora, two schemas
CMetaCorpus already existed (S1, report-only) with a proper SMetaCandidate
row. CSignalMETA kept a SECOND corpus of the same journaled candidates
right next to it: six parallel arrays, a second walk of the same 52 pattern
tables, a second row-filling loop, and THREE six-line ArrayResize blocks
keeping the six arrays the same length by hand. Same rows, same tables,
same meaning - and neither copy was reviewable without the other.

Now one class with one row schema and three sources:

  LoadFromConfigDb()   was Load(). This chart's fingerprinted DB via dbm;
                       what the S1 report reads.
  LoadLargestOnDisk()  moved in from CSignalMETA, header and all - the
                       symbol+period filter and the read-only open are the
                       point of it, and so is NOT going through the config
                       fingerprint (the trap that burned four corpus-build
                       runs). CountDbPatternRows moved with it as the one
                       "how big is this corpus" table walk.
  Add()                the on-chart ladder sweep, which needs the EA's live
                       filters and so stays in CSignalMETA - but stores here.

Storage is encapsulated: Count() is the truth, Reserve()/ShrinkToFit() are
hints, and every read is bounds-checked with an out-of-range answer that
cannot pass for a real candidate. That retires the sweep's hand-rolled
capacity block, which had already failed both ways - silently truncating
the corpus at a bar boundary on SP500, and running off the end mid-bar on
USDJPY/XAUUSD/XTIUSD because it only reserved room for two appends. The
lesson stays in the comment; the arithmetic does not.

SignalMETA.mqh 713 -> 573 lines. Also moved MetaPrepareEra's header
comment back above MetaPrepareEra - it had drifted two functions away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:41:47 -04:00
AnimateDread
303c9bf412 refactor(meta): the era's candidates are an object, not eight base members
Five parallel arrays, a count and a two-array intrusive chain sat on
CExpertSignalAIBase - inherited by every direction model, filled and read
by exactly one subclass. CMetaCandidateStore takes all eight.

What that fixes beyond the clutter:

- THE CHAIN WAS LINKED BY HAND. MetaPrepareEra wrote next[id] = head[bar]
  then head[bar] = id itself, after six ArrayResize calls it also wrote out
  itself. Add() does the linking, Reset() does the sizing, and a bar off
  the grid now cannot be stored at all rather than stored unreachable.

- THE BOUNDS TEST HAD FOUR SITES AND THREE IMPLEMENTATIONS.
  MetaCandidateWon indexed side[] with no test at all and answered
  "short" for any id out of range - the same shape as the ladder's
  negative-index read (2c351a0). Side() is three-state here, IsLong() and
  SideIndex() are the safe ways to ask, and the per-side era tally in
  RunOosPass is now guarded exactly like the per-family one beside it,
  which always was.

Like the ladder and the OOS tally, none of it needs a chart, a net or a
broker: hand it bars and rows and every answer is a function of those.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:36:31 -04:00
AnimateDread
3ea2bbc015 refactor(gate): the member gate and the ensemble gate were one rule written twice
SDeployVerdict::EvaluateRates() is now the deploy arithmetic - coverage
floor, chance + EDGE_MIN_SIGMAS x SE, tradeability, and the coverage-
discounted ranking score - and both gates call it.

The duplicate was self-documenting. The ensemble copy carried three comments
asking a reader to keep it in step with the member copy by hand: "same
intent as the member gate's coverage floor + bothSidesLive", "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 certifying a
vote the EA never casts, in the wrong currency and against the wrong
denominator.

THE TWO REAL DIFFERENCES ARE NOW ARGUMENTS, not branches:
  chancePct  - the ensemble filters its zero-skill reference by the
               direction policy, because with shorts blocked "always short"
               is not a book anyone could run.
  twoSided   - a member reads per-side RECALL against a floor; the vote
               reads whether it actually fired both ways.
Everything else was identical and is now literally identical.

effN stays an argument so the label-overlap deflation lives where it is
measured - and so the remaining inconsistency stays visible rather than
buried: the two FAMILY-WISE selection gates still take their SE from RAW n.
Recorded in the header, deliberately not changed; tightening them is a
policy call, not a refactor.

The decision now reads no chart, holds no net, prints nothing and opens no
file, so it can be exercised against a made-up tally.

BEHAVIOUR UNCHANGED: every expression keeps its formula, its guard and its
-1 sentinel; the ensemble's chance-reference and two-sidedness rules are
passed through untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:19:44 -04:00
AnimateDread
93d7bbe677 refactor(oos): twenty-one counters with one lifetime become one object
SOosTally holds this era's OOS confusion counts and the rates they imply.
The signal keeps one member where it kept twenty-one, and the era-reset
block loses twenty of its twenty-one clearing lines.

THE SHAPE THIS ENDS is the one that produced 7452bd1: a group of tallies
read together but cleared one-per-line, so a second reset path could clear
a subset and leave stale numerators over restarted denominators. Reset()
is now the only way to clear them and it clears all of them.

The pair had already started to drift. m_oosBuyFired/m_oosBuyFiredHits sat
at line 1085 and their Sell twins at line 1140 - 55 lines and an unrelated
member apart, with the Buy comment still claiming to describe both.

DERIVED RATES MOVE WITH THE DATA. `(bars > 0) ? (int)MathRound(100.0 * x /
bars) : -1` was written out twelve times, and the "-1 means not measurable,
never 0" convention re-spelled at each - a convention the deploy gate
depends on, since every caller tests `< 0` to mean "this does not block".
One rounding rule and one sentinel now.

GROUPED BY LIFETIME, NOT BY NAME. m_oosSamples looks like it belongs here
and does not: it is RUN-level, reset only with the weights, and the status
panel prints it beside dOosError which is also a run-level EMA. That pairing
is correct and stays. But the confidence-calibration block divided per-era
numerators by it, naming the results `empiricalAccuracy` and
`avgClaimedConfidence` when neither is that - the run-level denominator
cancels in their ratio, so eraScale was right and the two named
intermediates were not. Now written as the ratio it actually is, with the
cancellation stated, so nobody logs or gates on a half that decays with era
count.

BEHAVIOUR UNCHANGED: every moved expression preserves its formula, its
denominator and its sentinel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:15:22 -04:00
AnimateDread
2c351a02ca refactor(barriers): the ladder is an object, and its snap rule is one rule
CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages
plus the terminal travel) and every question asked of them. The signal keeps
one member where it kept three arrays and a lifespan scalar.

WHAT THIS ENDS. The log-space rung snap existed THREE times: once as
LadderRungFor, twice written out inline inside LadderWinShare - and
LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a
rung chosen here and a rung chosen there are the same rung". A comment asking
a reader to keep three copies equal by hand is the arrangement CMetaFamilies
was built to end. It is now one static RungFor(), so the two rungs agree by
construction.

The bounds test was spelled out at four sites and the "0 means never, tie
goes to the stop" comparison at three. Now Has() and FirstTouch(), once.
The four-site bounds test was also subtly weak: it computed
`idx * COUNT` and tested only the upper end, so a negative index slipped
through into a negative array read. Row() rejects it.

Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR
multiples; what a spread costs and how long the walk ran are facts the caller
supplies. Every answer is now a function of its inputs alone - which is the
point, because this is the barrier arithmetic that failed its own acceptance
test in b5e22a1 and it has never been runnable without a chart, a net and a
broker attached.

BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against
its predecessor with the rename map reversed; the only differences are the
substitutions named above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
AnimateDread
1d7ebbd67b fix(meta): a deleted loop's variable still decided every corpus row's side
The flat walk replaced `for(f) for(p) for(s)` with one TableAt() index, but
LoadMetaCorpus's row body still read `s == 0` to stamp m_corpusSide. The
compiler caught it - `undeclared identifier 's'` - which is the good case.

Worth naming the near-miss anyway: had an outer `s` been in scope, this
would have compiled and stamped every candidate with one side. The side is
now taken from TableAt's own isBuy, so the name that opens the table is the
name that labels its rows - one source, not two.

Also restores stdlib indentation at three sites where the removed nesting
left braces at the old depth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:55:09 -04:00
AnimateDread
37b3e0e36a refactor(pool): the cross-instrument gate owns a directory, not a model
PooledGate was three CExpertSignalAIBase method bodies in an #included
partial. It is now CPooledGate, a class the signal owns.

It needed NO data view. Diagnosing that first is the point: the module
reads a directory of CSV files and knows nothing about a model. The
only things it needs from its owner - the symbol's own numbers and the
ratio they were measured at - are arguments. Handing it a
CTrainingDataView would have been machinery for a dependency that does
not exist.

The owner fills SPoolRecord (the on-disk shape, which already existed)
because only it knows its symbol, its actual TargetRR and its label
lifespan. `id` is passed per call rather than bound, so there is no
init-order question about when the identity became available - m_symbol
is set by CExpertSignal::Init and ID by SetIdentity, at different
times.

m_poolWriteWarned was a one-shot latch living on the signal for a
warning only this module emits. It is m_writeWarned, private.

targetRR is now threaded into ReadPooledEvidence rather than read from
the owner. That is not plumbing for its own sake: a peer measured at a
different ratio has a different structural break-even, and only the
caller knows which ratio it is asking about.

Caught before compiling: I declared ReadPooledEvidence from memory as
(..., double &pooledEffN, const double targetRR). The real signature
ends in `string &detail`. Read the definition, aligned both ends.

Call sites in Training.mqh are untouched - PublishPoolRecord and
PooledGatePasses remain on the signal as the thin fillers that know its
geometry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:32:48 -04:00
AnimateDread
a699bd597d refactor(meta): one owner for the pattern taxonomy and its table names
MetaCorpus was already a class, so the raw-include problem was not the
one here. The problem was that the rule for naming a signal-DB pattern
table

    MetaFamilyName(f) + "_Pattern_" + p + ("_Buy" | "_Sell")

was written out FOUR times, each wrapped in its own identical
family/pattern/side triple loop: the corpus loader, the stale-DB guard,
META's row counter and META's exporter. Four chances for a rename to
leave three of them querying an absent table and reporting it as
"family disabled" - which is what that code says when a table is
missing, so the failure would have looked like normal operation.

CMetaFamilies now owns the taxonomy and that rule. Callers walk ONE
flat index over all 52 tables and never spell a name:

    for(int ti = 0; CMetaFamilies::TableAt(ti, table, f, p, isBuy); ti++)

Enumeration order is unchanged - Buy then Sell within a pattern,
families in order - so the corpus is assembled in exactly the same
sequence as before.

META's OneHotSlot had a second hardcoded 0/4/8/14 ladder with a comment
reading "matches MetaFamilyPatterns' 4/4/6/12" - a note asking a reader
to keep two constants in step by hand. The ladder is now summed from
the pattern counts, so they agree by construction. The bound against
META_ONE_HOT_SLOTS stays in META: the head's input width is that
class's business, and a taxonomy grown past it must be caught rather
than silently truncated.

Caught while re-reading the rewritten loop: my first counter was `t`,
and the body declares `MqlDateTime t`. Renamed to `ti` at all four
sites before it reached a compile.

MetaCorpus.mqh moves to Expert\Training\ with the other real classes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:26:33 -04:00
AnimateDread
a0f9cfa3e0 refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.

What the seam forced out into the open:

- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
  now one test per accessor next to the data. The two `hasValueN` and
  one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
  call site. It is now inside DataDirectionalCall, where it cannot be
  read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
  be turned into a side by any reader. The view answers
  DirectionalCall(bar, isBuy, magnitude) instead - the conversion
  happens where the head width lives, and the module no longer needs
  ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
  module makes. It is m_done, private, where it belongs.

Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.

The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
AnimateDread
2d8f231f12 refactor(arch): a read-only training-data view, so modules stop being #included code
The AIBase\*.mqh files are not modules. They are method bodies of one
3,400-line class, textually #included after its declaration. Every one
of them can touch every member of every other, which is why "move this
out" has so far meant "move the whole class".

Introduce the seam that ends that:

  CTrainingDataView   abstract - the ONLY thing a training-side
                      collaborator may see: a feature row, a label, an
                      outcome, an excursion, the shape they share, and
                      the identity to log under.
  CAIBaseTrainingData the adapter. MQL5 gives a class exactly one base
                      and CExpertSignalAIBase is already a
                      CExpertSignalCustom, so it cannot implement the
                      view itself. It owns one of these instead.
  Data*() on the      the published read API the adapter forwards to.
  signal              MQL5 has no `friend`, so reaching in from outside
                      was never an option - and making it explicit is
                      the point rather than a workaround.

Every row accessor OWNS ITS BOUNDS TEST and answers false for a bar it
has nothing for. Thirty-odd call sites currently carry their own
ArraySize() guard; one that forgets reads past a cache that is shorter
than the bar count for the whole warm-up. The -2.0 "never scored"
sentinel on the arrow cache is folded in the same way, so it can no
longer be mistaken for a small confidence.

Nothing uses it yet - this is the seam only, kept as its own commit so
the pattern compiles before 951 lines of Baselines move onto it. The
pattern is the stdlib's own: abstract base with =0 (Canvas\DX\DXObject),
concrete override, forward-declared owner pointer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:53:00 -04:00