Отслеживать
1
0
Ответвление
У вас уже есть ответвление Warrior_EA
0
ответвлён от animatedread/Warrior_EA
Граф коммитов Warrior_EA/Expert/AIBase/Lifecycle.mqh
Автор SHA1 Сообщение Дата
AnimateDread
a970405042 feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.

1. SP500 was training alone, and one alt-data column was the reason.

   The alt block's width joins the model fingerprint, and the pool reader only
   adopts peer rows whose fingerprint and width match. The exporter gives each
   instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
   13 - so the fleet ran as three incompatible pools:

     EURUSD/USDJPY/USDCAD  adopt ~57-60k peer rows each
     XAUUSD/XTIUSD         adopt 6.4k / 20.3k
     SP500                 "EVERY peer file was REJECTED, so this chart is
                            training alone" - 0 rows

   SP500 therefore trained on 2279 independent observations against a 600-wide
   input with its first layer floored at 16, printing its own "expect
   overfitting" warning. It is the one chart with no pool and the worst
   capacity ratio in the fleet by a factor of three.

   Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
   instead of their own file header. An existing model still adopts its .cfg
   pin, so this re-keys nothing that is already trained.

   Intersection rather than union: filling an absent series with its median
   makes that column constant per instrument, which lets a pooled model
   identify the source instrument and stop learning the shared mechanism. It
   is also 6 columns narrower. Cost is six columns whose retained information
   is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
   to names.

2. The MI keep-screen disabled itself for the whole run on any cold start.

   ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
   the label cache is allocated before it is filled, so BuildMiSample finds no
   row carrying a resolved label and returns 0 - a sixth exit, and the only
   one the 8c1266d instrumentation did not cover, which is why it printed
   nothing. observed then stayed -1, the permutation loop never iterated, and
   the report emitted "-1.00000 nats over 0 permutations" beside a plausible
   "strongest single feature 0.05979" that was a STALE m_miBestColumn from an
   earlier scoring call. The first ensemble member propagated the latch to
   g_ensembleChartMiReportDone and silenced every member on the chart.

   The flag now latches only once a measurement exists. A short sample is
   reported as a deferral naming the two numbers that identify it (cached bars
   vs bars carrying a resolved label) and retried, up to
   MI_REPORT_MAX_ATTEMPTS.

Build tag -> fleet-pool-v1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:41:14 -04:00
AnimateDread
d9092a2408 fix(vote): persist the member's skill verdict - a converged model was ruled no-skill on every restart
SP500 resumed converged at era 136 with its tier ladder correctly restored and still
swept 4999 bars reporting "0 had a snapshot, drew 0 arrow(s)" while the other five
charts drew 221-312.

HasDemonstratedEdge() - added with the no-skill exclusion - compares m_eraStatPrecPct
against m_eraStatChancePct. Both are written once per era by EnsembleStashEraStats. A
converged model runs no eras, so after a restart both sat at their -1 ctor defaults,
every member was ruled no-skill, ReconstructionWeight() returned 0 for all four, and
the overlay divisor was zero on every bar. Exactly the failure the WST7 ladder
persistence fixed one level down: the ladder says how much a member votes, this says
whether it may.

RankTiersFromOos already computes the pair (pooled holdout precision and the
zero-skill reference rate) and now records it as the CERTIFIED edge. That path is
reached by the era end AND by the deployed replay, which is the only measurement a
converged model will ever make. Persisted as WST8; HasDemonstratedEdge() prefers the
era pair and falls back to it.

The census line also had to be fixed: it reported "NOT ONE of those bars had a single
member snapshot ... no enrolled member has published m_overlaySigSnap" for a condition
that was purely a skill verdict. The snapshots were there. It now counts the two causes
separately and names the one that fired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:53:16 -04:00
AnimateDread
32eb5c5f58 feat(vote): edge-over-chance currency, no-skill exclusion, checkpoint burn-in
RETRAIN-FORCING and deliberately so. Two independent fixes for the same symptom -
charts that go quiet while others overtrade.

1. THE VOTE CURRENCY IS NOW EDGE OVER CHANCE, not an absolute win rate.

A tier weight is a raw win rate and a raw win rate means nothing without the
chance rate behind it: 30% is strong under a 14% base rate and catastrophic under
50%, yet both entered the mean as "30". That is why the threshold needed
re-tuning every time the label changed - 25 was permissive at ~70% win rates
under the old direction label and a near-unanimity rule at ~30% under the
pivot-event one - and why one chart's 25% was never the same statement as
another's. Subtracting the member's own chance rate makes the units percentage
points of demonstrated edge, comparable across charts, labels and regimes.

Clamped at zero: a below-chance tier is anti-informative, and contributing
negatively would act on a broken model as an inverted oracle rather than
discarding it.

2. A NO-SKILL MEMBER IS NOW ABSENT, NOT ABSTAINING.

Measured on XTIUSD: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5%
against a 14% chance rate - worse than guessing - and still voting. Three healthy
members voting Sell scored -21.06/0.77 = -27.4 and cleared; with the dead one
voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its
own ensemble on ~95% of bars, and that WAS the chart's 3.3% coverage. Neither
existing guard caught it: it IS self-ranked and its tier weights were 11-14.

The fix has to remove it from the DIVISOR, not just the sum - an abstainer
contributes weight by design, so zeroing only the contribution makes the dilution
worse. VoteCapableWeight() already means exactly "may this member's weight sit in
the denominator", so the skill test belongs there. ReconstructionWeight() and the
OOS scorer's divisor move with it or the scorer certifies a vote live does not
cast. The skill test reads the PREVIOUS era's measurement - gating this era's
vote on this era's own outcome would be circular.

3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).

XAUUSD deployed the checkpoint from ERA 2, XTIUSD from ERA 4, each after 69 and
65 further eras failed to beat it. Ensemble coverage measures AGREEMENT, and four
models that have barely moved off their initialisation agree almost by
construction - so coverage is inflated exactly when the models know least and
decays as they differentiate (XAUUSD 6.6% at era 8 -> 0.4% at era 75). Since
selectionScore is precision discounted by coverage, an early era outscores every
mature one and the ladder freezes on it.

INTENDED CONSEQUENCE: a chart whose MATURE coverage cannot clear the floor now
refuses to deploy rather than shipping era-2 weights. Fewer deploys, honest ones.

Burn-in eras are also kept out of g_ensCandidateEras (they could not have won, so
counting them inflates the family-wise N and raises the bar for nothing) and out
of g_ensErasSinceBest (or the run reaches "no better vote for N eras" with no
best to beat, exhausting the escalation ladder before the first era may compete).

Every pinned threshold and .stats record is in the OLD currency and is now
meaningless - this forces a fresh start on its own. Nothing needs re-tuning
because the threshold is DERIVED: the sweep re-picks the rung by itself.

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -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
781ae3a702 perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.

Three changes:

1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
   splits Save() into Snapshot() (the chart scan, in memory) and
   WriteSnapshot() (the disk half, consuming). New order: status label,
   vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
   then member sidecars, final sweep, timings, and only then the
   visibility file, the vote-arrow write and the weight saves.

2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
   CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
   prefix only when >=4 of OUR button names carry it, so a foreign
   dialog sharing stock chrome names is never touched.

3. m_netDirty: set by every net mutation (both backProp sites, both
   RestoreWeights sites, online learning conservatively, panel reset),
   cleared only on a successful Net.Save. Shutdown AND the per-bar
   autosave now skip the ~18MB write when the net is provably unchanged
   - for converged ensembles that is every save - which removes the
   very flood that starved the sibling charts. .stats still writes
   every time (small; carries the vote record and calibration). A
   skipped save leaves the .nnw header dtStudied stale, which is the
   already-handled attach-after-offline-gap case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
AnimateDread
4e4bff51d4 feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.

The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.

Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
AnimateDread
26fc9a1217 fix(replay): resolve labels inline - the prebuilt cache's window never overlapped the rescan
The 15:13 session proved the replay pass ran end-to-end on all 24 models
and scored ZERO labelled bars on every one of them, while each rescan sat
on ~5000 scored predictions (~2755 Buy / ~2232 Sell). The two windows
never overlapped:

  StartLabelCachePrebuild deliberately keeps a CONVERGED model's
  dtStudied watermark (it gates inference recency and must not move), so
  the prebuild's window was the handful of bars since the last studied
  bar - all with uncommitted pivots, hence "label cache pre-built -
  Buy: 0 | Sell: 0 | Neutral: 0" on every member.

The label never needed a cache. SwingPivotDirectionLabel(idx) is a pure
function of the ZigZag/Close/ATR buffers the rescan itself refreshes over
exactly the scoring window, and m_lastLabelLifespan == 0 is its own
unresolved flag - the same finality gate the cache applies, applied
directly. ScoreReplayFromCache now resolves each bar's label inline and
the label-prebuild stage is deleted from the rebuild state machine
outright; going through a cache built for a different window was
indirection that changed the answer.

Also splits the empty-result diagnostics: "no resolved labels" (a
windowing/data fault) is now distinguished from "labels present, every
call Neutral" (a calibration verdict). The first version reported the
second message for both, which mislabelled this very bug as a calibration
outcome in the same breath as reporting scored=0.

Honest limitation, stated in the code too: the replay window includes
bars the model trained on, so a replay-minted ladder is measured partly
in-sample and will read stronger than a holdout-measured one. It is
replaced by the genuine article at the next completed scoring pass; until
then it is what makes a restarted deployed model able to vote at all.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:22:01 -04:00
AnimateDread
5aec69fe4c feat(vote): replay pass rebuilds a deployed model's ladder without retraining
The previous commit persisted the tier ladder, which fixes this going
forward but did nothing for models whose .stats predates WST7 - they
still had to retrain to mint one. They never did. Every number a
converged model needs in order to vote is a pure function of weights
already on disk plus labels derivable from the chart, so replay them:

  stage 1  build the label cache   (existing chunked prebuild)
  stage 2  rescan history          (existing chunked rescan, deployed net)
  stage 3  score + rank + persist  (one walk over two arrays)

ScoreReplayFromCache() walks m_arrowSignalCache against
m_labelCacheBuy/Sell, fills the same m_oosTierFired/Hits and per-class
totals pass 3 fills, and hands them to RankTiersFromOos() - deliberately
feeding the existing ranker rather than reimplementing it. The shrinkage,
the chance reference and the module trust weight are subtle enough that a
second copy would drift, and a ladder measured by a slightly different
rule would be silently incomparable with every ladder training produced.

AdvanceDeployedRebuild() sequences the three stages off the timer. It has
to be a sequence: stages 1 and 2 are each minutes of work draining in
time-boxed slices, and stage 2's output is meaningless until stage 1 has
labels to score against. The previous version ran the rescan with no
labels at all, which is why it could only ever rebuild arrows and never
the ladder - the thing actually blocking the vote.

The result is written to .stats immediately. The failure being repaired
is state that lived in memory and was never written down; recomputing it
and not saving it would repeat that exactly.

Also routes every rescan completion through one hook, so there is a
single place that knows what a finished rescan means - republish for a
manual one, score and rank for a rebuild.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:01:24 -04:00
AnimateDread
b92e233b88 fix(chart): a deployed model rescans history to rebuild its vote arrows
The sidecar added in 484a9d8 restores the vote arrows from the previous
session - but there was no previous session to restore from, and a
deployed ensemble could never produce one.

The overlay that draws the vote layer replays each member's
m_overlaySigSnap, published in exactly one place: RankTiersFromOos, at
pass-3 completion. A converged model runs no further eras. So after a
restart every member's snapshot was empty, would never fill, the sweep
had nothing to replay and the chart stayed blank permanently - no route
back by any path.

The chart rescan is the route: it runs the DEPLOYED net forward over
history and rebuilds the per-bar cache, which is the same quantity pass 3
produces, obtained without training. It already existed for the panel's
Show-Signals button; it just never handed its result to the overlay, so
on the default filtered view a rescan rebuilt only the RAW per-member
layer - the one that is hidden - and appeared to do nothing.

- PublishOverlaySnapshotFromCache() extracted from RankTiersFromOos, so
  the era end and a completed rescan publish through one implementation.
- A completed rescan now calls it, which also arms the sweep.
- PollTraining auto-arms one rescan for a model that is converged, has no
  snapshot, and is on the filtered view. One-shot: a model that
  legitimately calls Neutral everywhere must not rescan forever chasing a
  snapshot that is correctly empty. On the timer, not in OnInit - it is a
  full feedForward per bar over up to 5000 bars and drains in the same
  time-boxed slices as a manual rescan.

Together with the sidecar this closes both halves: the rescan covers the
first session and any chart whose file was lost or invalidated by a
threshold change; the sidecar covers every session after one is saved.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:11:36 -04:00
AnimateDread
484a9d8b0f fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.

THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.

DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.

ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.

VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.

Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
AnimateDread
15827a6b77 refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.

The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.

Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.

ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.

RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -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
ad5c2542ec perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.

1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.

   A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
   did every parallel optimization agent, against the same file, with the
   per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
   SP500 H4 run: zero passes completed in 75 minutes.

   It bought nothing, for a reason specific to this EA's current shape: the
   DB's only effect on a trading decision is ApplyPatternWeight overriding a
   filter's module weight, and that is declined for any self-ranking filter
   (CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
   self-rank once their tiers are measured, and the classic votes that DID
   consume the ranking are gone - so a tester run's DB was written and never
   read. Skipping it changes no decision.

   One predicate, not two inline guards: OnInit asks the question twice
   (InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
   where those disagreed would try to open a database it never initialised.
   The tester now takes journal.InitTrackingOnly(), so close detection,
   MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
   dropped, and Update() already skipped its INSERT when there is no DB.

   Caveat recorded at the predicate: if a future filter consumes DB ranking
   WITHOUT self-ranking, this needs revisiting - a backtest would then stop
   reproducing live.

2. ExportFeaturesOnly and its two exporters are gone.

   Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
   rates grid), superseded by the research/ python path that reads its own
   data. Removed the input, m_exportFeaturesOnly, the setter, both method
   declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
   AutoTune.mqh), the OnTick early-return, and the ctor initialiser.

   The config-lock bypass it owned collapses to the plain tester test:
   `if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
   ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
   have other callers and are untouched.

Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -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
8c945bf752 feat(target): swing is the default, and tau is measured, not chosen
- TrainingTarget defaults to TARGET_SWING.
- LogitAdjustTau input, preset enum and all plumbing deleted: tau is fixed
  at 1.0 (the full log-prior, Menon et al.'s consistent value); the
  delivered strength is capped to the head's usable logit range from the
  priors the prebuild measures. The CAPPED journal line is the step-1
  measurement. |LA💯BS becomes a frozen legacy fingerprint slot, so no
  existing model re-keys.
- The swing label measures its own resolution lag (idx - P2, the earliest
  bar P1 can be final on) into the overlap/SE machinery, capped at
  SWING_SCAN_CAP_BARS instead of a barrier horizon it does not have.
- The prebuild line is target-aware: both-won, timeout and horizon-lifespan
  fragments are barrier-walk facts and no longer decorate swing counts.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 19:33:37 -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
cbd077c679 diag(training): report the window an era ACTUALLY trains on
With VerboseMode on, pass 1 reported eras of 422 / 949 / 1358 / 2562 bars on
SP500 H4 - four models, same chart, same second - against a series holding
~16,264 bars, and the number moved every era (CONV: 2562, 3671, 3405, 3532,
2830). Nothing in the journal said so. ReportDetectability and the CAPACITY
line both quote EstimatedInSampleBars, which is derived from the configuration
and not from the era, so they kept reporting "11385 in-sample rows / OOS window
4874 bars" for a window that was a tenth of that.

era.bars is MathMin(Bars(symbol, PERIOD_CURRENT, dtStudied, now) + historyBars,
Bars(symbol, PERIOD_CURRENT)). A short era is therefore either a dtStudied that
is too recent or a short price series, and those need opposite fixes - so the
new line carries all three quantities plus the resolved dtStudied and
SERIES_FIRSTDATE, not just the result.

Reported on change only: an era over a warm feature cache runs in a fraction of
a second here, and a per-era line would bury the journal.

Diagnostic only - no training behaviour is changed by this commit.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:12:05 -04:00
AnimateDread
6974fb03af ditch(features): remove the eight dead feature groups from the input matrix
RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta,
ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure,
WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a
closed verdict: the three oscillators are the same patterns that measured at chance
as entries, and the Wyckoff family returned zero out-of-sample on five independent
instruments - which is what closed the context score.

RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks
larger than it is. Every removed group contributed `flag ? N : 0` to the input
width, and every flag was false, so the width was ALREADY zero for all eight: no
.nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were
hashed unconditionally and become literal 0 legacy slots (the convention the
m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only
when enabled, so their segments simply never appear - byte-identical to every
fingerprint ever produced, since neither ever shipped on.

CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted
inside every .nnw, and Unflatten() rejects a size mismatch by falling back to
constructor defaults - so dropping the dead fields would silently revert the tuned
MA period of every model on disk while keeping its trained weights. That is the
feature/weight mismatch this project has already paid for twice, and it is not
worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and
read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing
searches them. The class comment says all of this at the declaration.

Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one
indicator now, and a name saying "AD" for the MA handle is the kind of stale label
that gets believed later. Its release-AFTER-recreate ordering is untouched - that
is a documented fix, not bookkeeping.

Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0
baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:21:03 -04:00
AnimateDread
e366bb74ad refactor(config-lock): CConfigLock is a real collaborator, not a raw-include partial
AcquireConfigLock/ReleaseConfigLock moved off CExpertSignalAIBase into
Expert/ConfigLock/CConfigLock, same view+adapter shape as BarrierHorizon/ExcursionHead.
Stateful: m_configLockName is exclusive (grep-verified, nothing outside Lifecycle.mqh's
old body touched it). Pure relocation - same FNV-1a hash, same owner-liveness check,
same log wording. Left uncommitted mid-campaign; independently compile-verified in
isolation now (0 errors/0 warnings) before this commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:39:17 -04:00
AnimateDread
b95ee72f4d refactor(labels): split the close-all budget / horizon ladder into CBarrierHorizon
Expert/AIBase/Labels.mqh (1807 lines) exclusivity-grepped almost entirely
SHARED: the label/win/excursion/ladder caches and the geometry-derivation/
prebuild state are touched with real per-bar array logic by Training.mqh's
hot era loop (m_labelCacheBuy/Sell/HasValue at 22+ sites), by FeatureScreen.mqh's
geometry scan (direct writes to m_barrierScanSlMult/TpMult, m_geometryAdopted,
m_geometryCfgSaved), by AutoTune.mqh and by SignalMETA.mqh - moving that state
into a collaborator would mean wrapping dense hot-loop array indexing behind
method calls across 5 files for no coupling reduction (same judgment already
recorded for AutoTune.mqh's remainder / Inference.mqh).

One genuinely closed sub-cluster survived the grep: the scheduled close-all
budget and the horizon ladder snap (NextScheduledCloseAll, MeasureCloseAllBudget,
EffectiveHorizonMax, RequiredHorizonBars, SnapHorizonToLadder, GrantedHorizonBars).
Only 2 fields are exclusive (m_closeAllCycleBars/m_closeAllMeanBudget - grep-
verified, Lifecycle.mqh's touch was constructor-init-list only) and NONE of the
6 methods has any external caller outside Labels.mqh (grep-verified whole-repo),
so nothing needed rewiring. New Expert/BarrierHorizon/: IBarrierHorizonView.mqh
(abstract, 4 accessors, 3 reused from the signal's existing Chart* getters, 1
new HorizonSwingMedianBars() wrapper) + AIBaseBarrierHorizonView.mqh/
AIBaseBarrierHorizonViewImpl.mqh (the adapter) + BarrierHorizon.mqh (CBarrierHorizon,
STATEFUL - owns the 2 exclusive fields as real members). Every method body is a
verbatim relocation (diffed programmatically against git HEAD modulo the field->
view substitutions - identical except one comment-wording update). The original
6 declarations on CExpertSignalAIBase became one-line forwards at their existing
position; Labels.mqh's own callers of these six needed zero changes since they
call them unqualified, which now resolves through the forwards.

Labels.mqh: 1807 -> 1643 lines. The rest of the file (label-cache population,
TripleBarrierLabel, DeriveBarrierGeometry, StartLabelCachePrebuild/
AdvanceLabelCachePrebuild, exit-policy simulation) is deliberately left as a
raw-include partial - not separable without relocating Training.mqh's era-loop
coupling, not reducing it.

Self-compiled 0 errors, 0 warnings (_claude_stage, ~94s).
2026-08-24 00:15:54 -04:00
AnimateDread
4dede6f6db refactor(features): FeatureBuilder is a real collaborator, not a raw-include partial
Expert/AIBase/Features.mqh (2017 lines, 38 methods) split by exclusivity grep
(whole-repo, not just Expert/): 30 methods -> Expert/Features/FeatureBuilder.mqh
(CFeatureBuilder + CFeaturesView/CAIBaseFeaturesView), 8 stay behind as a much
smaller raw partial.

CFeatureBuilder is STATEFUL, same shape as Excursion/OnlineLearning: owns the
10 feature-only indicator handles (m_Volumes/m_MA/m_RSI/m_MACDFeature/
m_Ichimoku/5 AD* CiCustom indicators - grep-verified touched nowhere else in
the repo, only their bare declarations) plus the depth-probe/handle-repair/
spread-series/detectability-latch scalars (exclusive, Lifecycle.mqh ctor-init
only elsewhere). m_Open/m_Close/m_High/m_Low/m_Time/m_ATR/m_ADZigZag stay
signal-owned - Labels.mqh/AutoTune.mqh/Training.mqh read them directly - and
are reached read-only through the view (FeatureOpenAt/FeatureHighAt/
FeatureLowAt/ChartBarClose/ChartBarTime/OnlineAtrMain, all reused where a
forward already existed).

Deliberately did NOT move InitOpen/InitClose/InitHigh/InitLow/InitTime/
InitADZigZag/ResizeBuffers/RefreshData: they manage the 7 shared indicators'
Create/BufferResize/Refresh lifecycle, which would need a pure-relay wrapper
per operation per indicator for zero coupling benefit - same judgment as
Topology's boot sequence. They stay in Expert/AIBase/Features.mqh and reach
CFeatureBuilder's 10 owned indicators through 20 new Feature*BufferResize()/
Feature*Refresh() forwards (signal calling into its own owned collaborator
directly, no view needed in that direction).

Whole-repo grep (not just Expert/) caught a real external miss the campaign's
own doctrine warns about: Signals/SignalMETA.mqh read m_spreadSeries/
m_spreadSeriesBars directly as an inherited protected field (a subclass, not
an AIBase/*.mqh partial) - fixed with two new FeatureSpreadSeriesBars()/
FeatureSpreadSeriesAt() forwards.

Verified: if(/for(/while( counts identical between the original file and the
new split (269/20/1); return-count delta (+12) fully accounted for by the 12
new trivial one-line forwards added (10 indicator BufferResize + 2 spread-
series getters); quoted-string-literal diff empty except two doc-comment
paraphrases. Self-compiled 0 errors, 0 warnings.
2026-08-24 00:00:31 -04:00
AnimateDread
c439878a82 refactor(topology): split shape derivation into CTopology, leave the boot sequence in place
Expert/AIBase/Topology.mqh (1191 lines) held two genuinely different jobs: the
fingerprint/derived-shape/BuildFreshTopology math, and InitNeuralNetwork/
InitFeatureIndicators - the network boot sequence (config-lock, tester-cache
seeding, load/save the .cfg, net-load backend fallback, chart/persistence/
online-learning orchestration).

Extracted the first job to Expert/Topology/ as CTopology + CTopologyView/
CAIBaseTopologyView (20 methods: BuildModelFingerprint, the Estimated*/Compute*
budget math, the Conv*/Lstm* shape helpers, Add*Stage, BuildFreshTopology).
STATELESS, like ModelPersistence - grep-verified zero exclusive fields, every
member these methods touch is shared elsewhere in the signal. Reused ~15
existing Data*/Chart*/Persist*/Exc* getters per the established convention;
added ~20 new getter overloads next to their existing setters (UseVolumes(),
MinDirectionalRecall(), etc. - same pattern as SignalClusterWindow) and ~16
new Topology*() wrappers for fields with no prior accessor. The Net-pointer
swap in BuildFreshTopology is one consolidated view call
(TopologyReplaceNetFromTopology), same doctrine as Persistence's
RunCpuInferenceSelfCheck - irreducible pointer work, not signal state.

Deliberately did NOT extract InitNeuralNetwork/InitFeatureIndicators: they
orchestrate nearly every other collaborator (chart, persistence, online-
learning, cross-asset, config-lock) rather than deriving a shape, so moving
them would just relocate a hub, not reduce coupling - same judgment call as
Inference.mqh (assessed, not extracted). They stay in the AIBase/Topology.mqh
partial, byte-identical to before (diffed against git HEAD to confirm), and
now call the extracted math through the same public forwards every other
caller already used.

Verified: string- and numeric-literal diff of the old file's 20 method bodies
against the new CTopology methods (0 differences), InitNeuralNetwork/
InitFeatureIndicators byte-identical, self-compiled 0 errors/0 warnings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 23:23:00 -04:00
AnimateDread
78a070eb5c refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).

STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.

Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.

Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:59:02 -04:00
AnimateDread
3278ea4ae2 refactor(excursion): ExcursionHead is a real collaborator, not a raw-include partial (S4)
Expert/AIBase/Excursion.mqh was 13 method bodies of CExpertSignalAIBase,
#include'd after its declaration - same "not a module" problem already
fixed for ChartUI (S2) and Persistence (S3). Extracted to
Expert/Excursion/CExcursionHead behind CExcursionHeadView/
CAIBaseExcursionHeadView, same view+adapter shape.

STATEFUL, unlike Persistence (0 exclusive fields): grep-verified 26
fields (m_excNet and every accumulator/trailing-ring field) touched
nowhere else in Expert\ except Lifecycle.mqh's old ctor-init-list
defaults and destructor deletes (now moved onto CExcursionHead's own
ctor/dtor). m_geo (SGeometryScan) and m_ladder (CFirstPassageLadder)
stay on the signal - both are genuinely shared with Labels.mqh/
Training.mqh at era boundaries - and are reached only through 15 new
Exc*() view wrappers, including one consolidated
ExcGeometryScanAccumulate() call (same doctrine as Persistence's
RunCpuInferenceSelfCheck) rather than field-by-field pokes.

All 13 original public methods stay at their same declaration point as
one-line forwards to m_excursionHead. Training.mqh's 2 raw m_excUs
reads now go through the new ExcursionMicroseconds() forward. Every
method body is a pure relocation, verified statement-by-statement
against the original (git show HEAD~1:Expert/AIBase/Excursion.mqh).

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:28:31 -04:00
AnimateDread
87382748d3 refactor(persistence): ModelPersistence is a real collaborator, not a raw-include partial (S3)
Expert/AIBase/Persistence.mqh (598 lines, 8 methods) -> Expert/Persistence/:
IPersistenceView.mqh (abstract, 68 read+write accessors) + AIBasePersistenceView.mqh/
AIBasePersistenceViewImpl.mqh (the adapter) + ModelPersistence.mqh (CModelPersistence,
the real collaborator - signal owns m_modelPersistence and binds it to m_persistenceView,
same shape as ChartUI's S2).

Grep-verified before starting: every field these 8 methods touch is ALSO touched
elsewhere in the class (Training/Lifecycle/OnlineLearning/Topology/FeatureScreen/
Labels.mqh) or already exposed via ChartView. Zero exclusive state, unlike ChartUI's
arrow-restore/rescan queues - CModelPersistence is stateless, holding only the
borrowed view pointer, operating entirely through 68 Persist*/PersistSet*() accessors
on the signal.

ValidateCpuInference's Net-pointer/throwaway-clone core is ONE consolidated view call
(PersistRunCpuInferenceSelfCheck) rather than field-by-field - irreducible pointer/
object work, not signal state, same doctrine as ChartScoreBarForRescan.
LoadNetWithRetry keeps its original CheckPointer(Net)-free Net.Load() call unchanged
(no guard added - would change failure behaviour on what must be a pure relocation).

This code writes the actual on-disk .cfg/.stats binary layouts every deployed model
depends on (explicit "DO NOT REORDER" comment in the original), so beyond compiling
clean (0 errors, 0 warnings) this was verified with a positional field-order diff:
every FileWrite*/FileRead* call's target field, extracted and normalized from both
the original and the new file, matches 1:1 in the same order (43/43 on the write
side covering SaveModelStats+SaveTopologyConfiguration, 17/17 on LoadModelStats'
read side; LoadAndCompareTopologyConfiguration's local-variable read block was
copied verbatim, untouched, so nothing to diff there). The magic-version
conditionals (WST2-6, haveDerivedStages/haveBarrierGeometry/etc.) moved unchanged.

All 8 methods keep their exact original signatures as one-line forwards - zero
external call sites changed.
2026-08-23 21:45:09 -04:00
AnimateDread
053d704a84 refactor(chart): ChartUI is a real collaborator, not a raw-include partial (S2)
Expert/AIBase/ChartUI.mqh was 869 lines of method bodies of
CExpertSignalAIBase, #included after the class declaration - free to touch
any of its ~500 members. First of the eleven AIBase/*.mqh partials to come
out (fewest inbound edges - see the SOLID campaign session order), using
the same view+adapter shape already proven for CTrainingDataView.

CChartView (Expert/Chart/IChartView.mqh) is the abstract read/behaviour
surface a chart-rendering collaborator needs - identity, bar/model access,
the prediction cache, and the training/vote/meta scalars the panel and HUD
line summarise. CAIBaseChartView is the adapter the signal owns and binds
to itself (MQL5 gives a class exactly one base, so CExpertSignalAIBase
cannot implement the view directly). CChartUI is the real collaborator: it
owns the arrow-restore queue, the rescan queue/tally, the last-arrows-saved
count and the purge-mismatch latch as its own fields (verified via grep to
be touched nowhere else in Expert/), and reaches everything else - including
StartChartSignalRescan, moved in from its old inline home in the header
since it drives the exact same rescan state machine AdvanceChartSignalRescan
drains - through the view.

m_arrowSignalCache and m_signalClusterWindow stay on the signal: Training.mqh
writes the cache directly every era and the training-data view already reads
it, so moving it would mean rewriting Training.mqh's write sites too - out of
scope here. CChartUI reaches it through four bounds-checked accessors instead
of a raw member poke. All 10 public methods keep their exact signatures and
become one-line forwards on the signal, so no other file's call sites change
except Training.mqh's one era-end status refresh, which now reads
RefreshStatusLabel() rather than reaching into CChartUI's now-private
last-displayed-neuron cache directly.

Verified structurally, not compiled (never compile - the operator does, in
MetaEditor): brace balance checked on every touched/new file against HEAD,
and the view/adapter/impl method lists cross-diffed to confirm all 59
accessors match 1:1 across the interface, the adapter declaration and the
adapter body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:03:52 -04:00
AnimateDread
909f2385bc refactor(build): retire the WARRIOR_EXPORT_FEATURES compile flag
Last surviving compile-time feature switch in the codebase - the same pattern
already killed for the MARKET build and DirectML tier (02766b5): one build,
configured at runtime like every other module (inputs + getters/setters, set
in ConfigureAISignal during OnInit), not a second code path that only existed
if someone remembered to define a macro before compiling.

Replaced with `input bool ExportFeaturesOnly = false` (Variables/Inputs.mqh)
and a plain m_exportFeaturesOnly member + setter, matching AutoTuneIndicators'
exact shape. Four call sites converted from #ifdef to a runtime read of the
same variable:
  - Warrior_EA.mq5 OnTick() - reads the input directly (this check has to
    stand before any per-signal object exists)
  - Topology.mqh's config-lock skip and ExportFeatureMatrix() call - read
    m_exportFeaturesOnly, now set by ConfigureAISignal before InitIndicators()
    runs (same init-order guarantee AutoTuneIndicators already relies on)
  - ExportFeatureMatrix()/ExportRawRates() declarations - always compiled now,
    called conditionally instead of not existing as symbols

No change to what the flag does when off (the state of every build that
exists today, since the macro was never defined anywhere in-repo) or when on;
only how it's set. Verified: WARRIOR_EXPORT_FEATURES fully gone from every
#ifdef/#endif in the tree; brace and ifdef/endif counts balance in every
touched file; ConfigureAISignal runs before StepInitIndicators in OnInit's
linear init chain, so the flag reaches InitNeuralNetwork() in time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:14:55 -04:00
AnimateDread
fec4d47eb2 refactor(labeling): CTripleBarrier - one copy of the fill/barrier arithmetic
Session B of the feature-selection/labeling refactor track. Extracts the two
pieces of triple-barrier arithmetic that were genuinely duplicated or
scattered, taking price/ATR/geometry as plain arguments - no chart, no
indicator handle - so it is testable with synthetic numbers.

CTripleBarrier::ComputeLevels() replaces the fill/barrier level arithmetic
that TripleBarrierLabel() and SimulateTradeOutcome() each spelled out by
hand; their own comments already called it "IDENTICAL... deliberately and by
copy." One caller resolves both sides at once (the both-won tie-break needs
both); the other selects the side its isLong argument names. Same for
ApplyMinStopWidening(), the broker-minimum-stop floor both walks applied.
Fuzzed 200k random (entry, spread, risk, reward, minStop, isLong) tuples
against both original hand-written forms: 0 mismatches.

CLabelOverlap replaces m_labelLifespanSum/m_labelLifespanCount - two members
reset from three separate call sites (constructor, label-cache rebuild), the
exact "N loose members cleared in more than one place" shape a candidate-
geometry incident (7452bd1) turned into a live bug. One object, one Reset(),
default-constructed like every other object member. MeanLabelLifespan() and
EffectiveSampleSize() on the signal become thin forwarders with an unchanged
signature - every one of their ~15 existing callers, direct and through the
CAIBaseTrainingData adapter, is unaffected.

SnapHorizonToLadder() forwards to CTripleBarrier::SnapToLadder(), the ladder
array's one remaining copy; EffectiveHorizonMax() (the close-all cache) stays
on the signal since that state has no clean argument form.

NOT extracted: TripleBarrierLabel()'s ~200-line walk itself. It resolves both
sides simultaneously, tracks the first-passage ladder, and feeds the label
every live order is sized from; a rewrite of it cannot be checked without a
compiler, so only the two pieces provably identical to their originals moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 18:57:17 -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
3d2ee517ca refactor(meta): the veto is a gate, not a virtual every signal carries
Since S3 (f64e0f8) the meta head casts no vote - it scores an entry the
consensus already cleared and vetoes the ones under the cost-adjusted
break-even. The code still said otherwise. LiveMetaGate() was a virtual on
CExpertSignalCustom, so MA, RSI, MACD, Ichimoku, the four direction nets,
the session and news filters and the risk guard each carried a meta-gate
method they had no business having; one class implemented it and a dozen
inherited it. The trading pipeline held the gate as a CExpertSignalCustom*
- a signal pointer, with a signal's two hundred other methods reachable
from the entry path.

Expert\Trading\MetaGate.mqh now owns the abstraction:

  CMetaGate            one pure virtual, Evaluate(), and the two static
                       readings of a verdict (Blocks / Scored)
  META_GATE_*          names for the four codes the three call sites used
                       to spell as bare 0/1/2 and test three different ways
                       (`< 0` here, `== 2` there, `else` for the rest).
                       Codes unchanged; only ONE of them blocks, and that
                       asymmetry is now stated where it lives.
  SMetaGateTelemetry   the five m_metaGate* members that were on the AI
                       signal base - inherited by every direction model,
                       meaningful for none of them. One lifetime, one
                       writer, one object; the arm latch and the two
                       counters are a set that clears together.

g_warriorMetaGate is a CMetaGate*. MQL5's single inheritance means the head
cannot also BE one (it already extends the AI base for the net, the era
loop, the feature windows, the label caches and persistence), so it owns a
bound CMetaGateAdapter and hands that out - the same shape CTrainingDataView
uses for the same reason. LiveMetaGate() is gone from the signal base.

Behaviour unchanged: same codes, same thresholds, same fail-open doctrine,
same live-only telemetry rule. The adapter fails open when unbound, on that
same doctrine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:33:47 -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
7452bd1ba9 fix(geometry): a shutdown abort cleared one tally of ten
Found by grouping, not by looking for it.

The candidate-geometry scan kept ten accumulators as ten separate
members. Era start cleared all ten in a ten-line block. The
shutdown-abort path inside the exit-policy simulation cleared
m_geoTrades and nothing else, so nine partial sums - diffSum,
diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen,
startTick - survived the abort with the aborted era's values.

The next era then accumulated onto those sums while counting from
zero, so the paired mean is sum/trades with a numerator carrying an
extra era's worth of difference. The paired sigma is worse: diffSumSq
inherits the same contamination, so the scan reports a tighter or wider
spread than it measured depending on what the abort happened to be
holding.

That is the SAME arithmetic that failed its own acceptance test in
b5e22a1, where the reported gain turned out to be monotone in timeout
share. This is not that bug - it needs a shutdown mid-era to fire - but
it lands on the same number, and any geometry reading taken from a
session that was stopped and restarted is suspect.

SGeometryScan now owns all ten with one Reset(). Both sites call it.
A partial reset is no longer something that can be written: there is
one door, and it clears everything behind it.

The struct initialises itself, so the ten constructor-initialiser
entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields
there, which is a second reason ten loose members was the wrong shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49: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
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
AnimateDread
788115970c fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".

1. THE DRAW. The filtered overlay armed on the FIRST member to finish
   pass 3 and leaned on a 60 s rate limit to "collapse the burst",
   assuming members finish seconds apart. They do not - on USDJPY one
   member was at sample 10496 of pass 2 while another was at 2304,
   minutes apart. A member with no era-end snapshot returns false from
   SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
   BEFORE `den += ModuleWeight()`, so the one finished model's tier
   weight became the entire vote and was drawn as a consensus arrow.

   An abstention is a member that looked at the bar and said nothing; a
   missing snapshot is a member that has not looked. The first must
   dilute the vote, the second must suppress the draw. The arm is now a
   readiness MASK - one bit per m_ensembleIndex, set at that member's
   pass-3 completion, cleared when a sweep arms - and a sweep waits for
   every enrolled member. Bounded at 10 minutes so a member that stops
   cannot freeze the chart, and the partial draw PRINTS which members
   were missing: the be39674 lesson is that a hold must never silence
   the thing that reports it.

2. THE VOTE ITSELF, which is the worse half and is not display-only.
   Tier weights are not persisted in the .nnw - they exist only as the
   output of a completed pass 3 - so before a member's first
   RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
   Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
   call enters the capability-weighted mean claiming a 100% win rate
   beside ranked members contributing ~25. Not a strong opinion: the
   wrong unit. One unranked member drags the ensemble over any
   threshold, on every fresh deploy and every resume. USDJPY has a
   measured ceiling of ~19 and was firing anyway.

   LiveVoteContribution() now abstains until self-ranked, which drops
   the member from the sum AND the divisor. One function, so live and
   the gate move together (2c443ba).

Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
AnimateDread
042f20bdb9 fix(barriers): cap the horizon at what the close-all actually grants
The diagnostic shipped in de382bb came back off both live charts and
confirmed the arithmetic exactly:

  CLOSE-ALL BUDGET - flattens every position every 29 bars ... an entry
  landing anywhere in the cycle gets 15 bars on average. The horizon
  ladder just granted 128.

So the ceiling the ladder was rejecting rungs against - BARRIER_HORIZON_MAX,
384 - never bound anything, while the one that does bind was invisible to
it. SnapHorizonToLadder and the scale ladder's fitsH test now both read
EffectiveHorizonMax(), which is the measured close-all cycle. One
function, so the ceiling cannot be lowered in the snap and left high in
the rejection test.

The CYCLE, not the 15-bar mean: a Monday entry really does get the whole
cycle, and rejecting on the mean would invent a second criterion where
the design deliberately has one ceiling and reports the milder snap-down
truncation instead of rejecting on it.

Expect the ladder to pick a NARROWER pair, which is what the MEASURE
objective already asks for - min provable EV grows as width squared, and
USDJPY's 6.00*ATR target was being asked of a trade that lives ~11 bars.

"Schedule off" is cached; "not enough bars loaded yet" is not. Caching
the latter would restore the 384-bar ceiling for the whole process
because one early call landed before history arrived.

RE-KEYS EVERY FINGERPRINT - the horizon is a label parameter, so this is
a full retrain on both charts. Done now because both are at era 0 after
a fresh deploy, which is the cheapest this change will ever be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:32:13 -04:00
AnimateDread
b5e22a1e34 fix(geometry): a free zero made "never resolve" the winning geometry
The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate
geometry beats the global pair on every SP500 member at 2-3 sigma. It
does not. It said so because a bar that reached neither barrier scored
0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a
losing baseline a free zero is a win, so the widest candidate always
came out ahead - and the reported gain ordered itself by timeout share,
not by skill:

  PAI  95.1% timed out -> +0.189 R   (head measured -2.42 sigma, HARMFUL)
  HYB  73.8%           -> +0.182 R   (head at chance, +0.68 sigma)
  CONV 61.8%           -> +0.163 R   (head measured -2.47 sigma, HARMFUL)
  LSTM 27.1%           -> +0.158 R   (head +1.67 sigma)

Monotone in the timeout share and inverted against the sigma gate. The
acceptance test written when this was built - "the sigma gate predicts
LSTM helps and CONV hurts; if the R difference does not reproduce that
ordering, something is wrong" - is what caught it.

A trade that reaches neither barrier is not worth zero. It is closed at
the horizon, which is what the scheduled close-all does live and what
SimulateTradeOutcome's timeout path already charges. So mark it there:
TripleBarrierLabel now publishes the signed close-to-close travel at the
last bar it actually visited (m_termTravelCache, same validity flag as
the excursion and ladder caches), and LadderOutcomeR prices a timeout
off it instead of returning false. A bar that cannot be evaluated under
BOTH pairs is now dropped whole - scoring one leg and defaulting the
other is the same bug in a smaller costume.

Second defect, same function: CandidateGeometryFor applied neither of
the floors the global derivation applies, so on USDJPY it chose stop
2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy
floor. c3daded in miniature: a selector optimising its own criterion
with no reference to the decision criterion. Both floors now apply, and
the ratio is re-checked AFTER the per-leg rung snap, which can lose it.

Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM
fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 -
41% of the ensemble's capable weight and the loudest voice on the chart,
off two effective observations. It also lifted the computed vote ceiling
to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never
printed on a chart whose peak vote is 14 and whose practical ceiling
without that member is 18.8. The pooled rate is now shrunk toward the
coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls,
and the tiers shrink toward the shrunk value rather than the raw one. A
member with ~300 effective calls moves by ~0.4pp; the 19-fire member
goes 0.37 -> ~0.15.

MEASUREMENT ONLY still - no order reads any of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
AnimateDread
05f1a539c4 feat(geometry): measure per-candidate barriers against the one global pair
Stage 2a of the candidate-conditional geometry the record has named as next and
never built. MEASUREMENT ONLY - no order uses it yet.

WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed
geometry, and its verdict stands: real skill, 0 operating points clearing
break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion
head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and
beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so
the lever is the geometry, not the veto. A per-candidate rung means a
per-candidate break-even, which a binary gate cannot express.

HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent
pair AND under the pair this bar's excursion head would choose, and the paired
difference is accumulated in R with a 2-sigma test. Both legs come from the SAME
first-passage ladder - four array reads, no re-walk, exact even on the ~28% of
bars where both barriers were touched. Mixing the ladder with the price walk
here would measure the discrepancy between two of our own evaluators rather than
the effect of the geometry, which is precisely what f8ac10c had to unpick one
layer over.

The candidate pair applies the GLOBAL derivation's own rule per bar: stop at
BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable.
Neither creates expectancy; what moves is the break-even, which is why the
report quotes R and never a win rate.

FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R
difference reproduces that ordering across members, the head's usefulness is
confirmed by a second, independent measurement. If it does not, something is
wrong and this must not be wired to orders.

Two things caught while writing it, both silent if missed:
- The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is
  risk = ladder + spread but reward = ladder - spread, so the two legs convert
  with OPPOSITE signs. The stop leg had the sign backwards.
- A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and
  a head forward per OOS call to a walk that already runs unchunked at era end
  on a single-threaded EA. That is the shape that got the process force-
  terminated on 2026-08-21. It stops scoring, never the replay, and the report
  prints how many calls it covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
AnimateDread
b91c7b1f7a refactor(comments): box headers to stdlib length
The //| box blocks were excluded from 0b06f8e and 5efdb48 and were what
remained: 160 of them ran to 10+ lines, the longest to 88. Compressed to their
leading topic sentences - 5 lines for a function header, 8 for a file header -
keeping the box format and the standard MQL5 name/author lines verbatim.

Verified at the BYTE level this time, across every in-scope file: the list of
non-comment lines is byte-identical to HEAD and braces balance. The first check
compared a locale-decoded 'git show' against a UTF-8 read and flagged 25 files
that had not changed at all - every BOM and every non-ASCII line mismatched.

47,696 -> 40,665 lines in scope; comment share 38% -> 26%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:30:14 -04:00
AnimateDread
5efdb48de4 refactor(comments): stdlib comment style across the remaining in-scope files
Same pass as 0b06f8e, applied file by file: comment runs of 4+ lines compressed
to their leading topic sentences, capped at 4 lines, whole sentences only.
Warning sentences (NEVER / MUST / trap / would-have) survive the budget.

Every file was checked the same way before committing: the list of non-comment
lines is byte-identical to HEAD, and braces balance. No code was touched.

Panel/, Enumerations/ and the already-terse System headers needed little or
nothing - PooledGate, TradeChecks, BinomialStats and Random came through with
no blocks over the threshold at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:25:52 -04:00
AnimateDread
ee4d459c6a fix(excursion): the sample gate asked for more windows than the configuration can contain
EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar
horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the
history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so
200 was unreachable and every era printed "[disjoint sample too small]", which
an operator reads as "wait longer". Unpassable by construction - the identical
failure this file already documents one gate down, one layer up.

The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars,
so it failed the same 200 for the same reason, at 130.

Raising OOSSplit or shortening the horizon would clear it and would be fitting
the experiment to the answer. Instead, ask the question the count was standing
in for - is the skill bigger than its own noise:

- The scorer banks one paired Brier difference per DISJOINT window over the
  decision rungs (base-head, and trail-head). Disjoint by construction, so no
  EffectiveSampleSize deflation applies - striding by the horizon is what buys
  that - and paired on identical bars, so the correlation between the two
  predictors cancels instead of needing to be estimated.
- passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND
  >= 2 sigma, with the count reduced to a sanity floor of 30.
- Both sigmas print on the verdict line.

This is not a lowered bar. The 2% skill requirement, the oracle control and the
monotone test are untouched, and the sigma test can fail where the count test
never spoke: if +8.5% is noise across 147 windows, it will now say so.

DecisionRungMask() is the single definition of "rung the decision depends on",
called by both the scorer and the report, so the standard error is computed
over exactly the rungs the skill score is. The report's inline copy of the
bracketing test is gone.

Also prints whether the disjoint count is BELOW ITS CEILING or at it, so
"not enough yet" and "not in this configuration" stop reading the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:06:49 -04:00
AnimateDread
f8ac10c808 fix(replay): the exit replay held trades through the Friday flat that the label and the live EA both close
The EXIT-POLICY REPLAY line reported an expectancy from SimulateTradeOutcome
beside a win rate read out of the label cache, and called them "the SAME
calls". Same calls, two different walks - and the walks did not agree.

TripleBarrierLabel stops at NextScheduledCloseAll (3e467f9); SimulateTradeOutcome
never called it, so the replay kept holding positions the live EA is flattened
out of and collected targets the label had already scored as cut. On SP500 H4
the simulation's implied win rate ran 2.2-3.4pp above the label's on identical
calls, and the timeout share read 0.8-1.3% because nothing was truncating the
horizon it walked.

That gap, plus 1.4pp of spread charged twice in CostAdjustedBreakEvenPct, is
the whole of the ~5pp the replay looked "off" by. It was not horizon timeouts,
which is what 4070c5c argued and this log disproved: solving E[R] = 3.008w - 1
+ t(1+m) on each row puts the simulation's zero-crossing at an implied 33.3%
against a frictionless 33.24% - it was internally consistent all along.

- SimulateTradeOutcome takes the close-all cutoff, same expression and same
  placement as the label's, falling through to the existing close-at-last-bar
  branch. Expect the timeout share to rise and expectancy to fall: the replay
  was optimistic.
- m_simTpHits counts this walk's own target-before-stop, printed next to the
  label's with the delta, so a future divergence is visible rather than
  inferable.
- The line prints all three break-evens and names the R convention. The
  frictionless figure is the one this expectancy crosses zero at, because both
  walks place the barriers off the spread-shifted fill.
- CostAdjustedBreakEvenPct is left alone: it still feeds the rung selector's
  BarrierMinReachPct, and moving that relabels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:08:26 -04:00
AnimateDread
4070c5c9bd feat(breakeven): the break-even every layer scores against prices a trade that always resolves
CostAdjustedBreakEvenPct is risk/(risk+reward) and has no horizon term. It is the win rate a trade
needs when it is CERTAIN to end at one barrier or the other. SimulateTradeOutcome has an explicit
branch for the case where it does not - runs out of horizon, closes at the last bar seen for
whatever P&L that is - so on this label geometry the figure describes a different trade than the
one being replayed.

The gap is measurable and large. Across 21 exit replays today on SP500 H4 the geometric figure read
34.5% while the EA's own R simulation crossed zero between 27.4% (lowest positive) and 28.9%
(highest non-positive). Independent corroboration: the zero-skill reference, computed empirically
over every scored bar as max(winLong,winShort)/bars, reads 25.4% - add cost and it lands on the
same ~28%. The geometric number is the outlier, and every edge printed against it was ~6.5pp too
pessimistic: LSTM's 30.6%-win era reported -4.0pp while its replay returned +0.075 R on the same
trades.

With a timeout share t paying a mean m R apiece, expectancy is w(1+RR) + t(1+m) - 1, so

    w* = (1 - t(1+m)) / (1 + RR) = CostAdjustedBreakEvenPct x (1 - t(1+m))

which needs no new geometry - the existing figure already carries 1/(1+RR).

This commit MEASURES ONLY. The replay now separates timeout exits from barrier exits and latches
t and m for the next era to read (the accumulators are zeroed at era start and filled at era end,
so a mid-era reader sees zero trades and would fall back forever). Both break-evens print side by
side on the replay line with t and m beside them, and the threshold line's REPORTED edge - which
selects nothing - switches to the horizon-aware figure so the operator stops reading a wrong sign.

DELIBERATELY NOT CHANGED: LiveMetaGate's veto and the rung selector's BarrierMinReachPct still read
the geometric value. Both are decisions - the second re-derives geometry and therefore relabels -
and t and m have so far only been inferred from a zero-crossing, never seen on a log. One era of
this instrumentation settles that.

The file already contained the argument, one branch away, in the vote-exit comment: a vote exit
produces a CONTINUOUS payoff, not a win or a loss, and that is why an exit-aware gate cannot go on
scoring win-rate against a fixed break-even. A horizon timeout is the same thing, and unlike vote
exits it is on by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:26:25 -04:00
AnimateDread
2de62241a5 fix(init): the OnInit failure the operator reads did not name the reason it failed
Two same-config charts collide on the config lock. AcquireConfigLock prints a precise REFUSED
line, but CExpert::InitIndicators then returns a bare false, RetryInitStep retries it five times
- a lock held by a live chart gives the same answer every time - and the last line on screen is
"Failed to initialize Indicators after retries", which names neither the lock nor the owner.
The explanation is six lines further up, under identical-looking retry noise.

A refusal that cannot change on a retry now says so and stops: AcquireConfigLock records the
reason, RetryInitStep repeats it and returns immediately.

Found because a second SP500 H4 chart was started to run Run_Alglib_Baselines. That input is a
diagnostic and is deliberately not in the fingerprint, so both charts resolved to the same
filename - the guard was correct and the message was not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:12:23 -04:00
AnimateDread
667f2bcb6b revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts a863796 on the operator's call - "unnecessary complexity". It was
right about the mechanism and wrong about the priority: it re-cut the classes
for a case the measured verdict never reaches (SP500 H4 reads "both sides" at
the derived geometry), while the drift that IS happening affects every chart
and every era. Recoverable from a863796 if a one-sided book ever becomes real.

Two pieces of it survive, both independent of the exit idea:

The drift verdict keeps reading m_winLongCache/m_winShortCache rather than the
collapsed label pair. That line reports always-long vs always-short win rates,
which is what the win caches hold - each side scored on its own barriers,
published before the collapse. The label pair carries only the side touched
first, so it undercounted long wins by the both-won-goes-to-short share. There
are zero both-won bars at any geometry with target >= stop, so this changes no
number today; it changes the wrong number to the right one.

And the .cfg gains nothing and loses nothing: the two appended ints go away
again, and they were the last fields, so a .cfg written by yesterday's build
still reads correctly - the loader simply stops before them.

WHAT THE REVERT MAKES ROOM FOR. The operator's actual requirement is that the
model reproduce the label distribution the scan measured, and nothing in the
pipeline ties it to that. The loss trains on a rebalanced sample and the
abstain rate is owned by a margin threshold fitted on EDGE, so the call rate
and the label prior can drift arbitrarily far apart - and did, invisibly:
at era 1350 the models call Buy on 20-28% and Sell on 22-32% of bars against
a scan-measured 2.1% and 4.8%. Roughly a 10x over-call, and not one line in
the journal said so.

The era line now carries it:

  CALIBRATION calls vs true rate Buy 28% vs 2% (14.0x) Sell 32% vs 5% (6.4x)
  Neutral 40% vs 93% (0.4x)

Reported as a ratio because that is the readable number - 1.0x is calibrated.
This is deliberately a measurement and not yet a correction: matching the
label rate would put coverage near 7%, below the ensemble gate's own 12.4%
coverage floor, so calibration and the gate are in direct conflict and which
one yields is the operator's call, not mine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:14:26 -04:00
AnimateDread
68ef19797b fix(telemetry): one ensemble member had never printed a single era line, in any run on record
The era-progress rate limit was a function-scope `static`:

    static uint lastProgressLogTick = 0;
    shouldLogProgress = (nowTick - lastProgressLogTick >= 5000);

In MQL5 that is ONE variable for the whole build, not one per object. A 5s
limit meant to keep a single model's console readable was therefore a limit
across the WHOLE ENSEMBLE, and it did not distribute fairly - it starved
whichever member finishes last, every era, deterministically.

Measured on today's run: the era barrier releases the members together and
LSTM landed 2.06s, 2.43s and 2.28s behind ConvLSTM on eras 1-3, against a 5s
window it could never reach. LSTM printed zero era lines. PAI, CONV and HYB
printed all of theirs - 3 each this run, 17 each in the 10:00 run, LSTM 0 in
both, and 0 again in the 08:32 run.

So one model in four has been training with NO per-era telemetry: no
per-class recall, no dW/W ratios, no zero-skill comparison, no deploy-bar
line. It was still doing the work - tier re-ranks, threshold fits and
exit-policy replays all appear on cadence - which is what made the hole look
like a grep that kept missing the line rather than a line that was never
written. It cost me the LSTM half of a gradient check earlier today.

Now a member, so each model rate-limits its own console output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:48:56 -04:00
AnimateDread
a86379621c feat(labels): on a one-sided book the blocked side's class is retargeted from an entry it can never take to the EXIT of the one it holds
User request: "when an asymmetry is noticed in a market (like sp500 upward
drift) ... it does not need to predict shorts, but exit points. a sell signal
needs to be preceded by a buy so that it can say I predict we must close that
long."

Until now a LONG_ONLY verdict only BLOCKED short entries. The network went on
being trained to predict them - a third of its output capacity spent learning
an answer the direction policy guarantees it can never act on, while the
question the book actually faces (when to get out of the long) was never
asked. The two are not the same event: "a short pays" needs price to travel
the SHORT's target before the SHORT's stop, and at any geometry where reward
!= risk that is a different bar from "this long hits its stop first". The exit
is the second one.

So on a one-sided book TripleBarrierLabel re-cuts all three classes around the
only position the book can hold: Buy = it reaches its target, Sell = it
reaches its STOP first, Neutral = the horizon expired with it still open. Both
come off the allowed side's own barriers, which the walk already computed -
this reads longLost where it used to read shortWon, so it costs nothing.
Label lifespan and the timeout flag follow the allowed side too, so the
overlap correction is sized on the window this label actually spans.

DECIDED ONCE, AT ERA 0, AND PINNED. m_exitTargetSide goes in the .cfg beside
the derived geometry under the same doctrine and for the same reason: it
decides what Buy and Sell MEAN, and a target that moved mid-run would retrain
a fitted model against something it never saw. A .cfg from before this ends
early and reads 0/0 - "not decided, symmetric" - which is exactly what every
existing model was trained as, so nothing needs migrating. The weights
fingerprint keys on the INPUT only (explicit Long only / Short only); under
Intelligent the measured verdict must never reach a filename, or the model is
orphaned the moment more history downloads.

THE DRIFT VERDICT HAD TO MOVE OFF THE LABELS FIRST, and it turns out it was
measuring the wrong thing anyway. It counted m_labelCacheBuy/Sell and called
them "always-long vs always-short win rate", but the label pair is the
COLLAPSED first-touch verdict: a bar where both sides reached their target
carries only the side touched first, so long wins were undercounted by the
both-won-goes-to-short share. m_winLongCache/m_winShortCache are the actual
per-side win rates, published before the collapse, and that is what it reads
now. Necessary as well as more correct - deriving the verdict from labels the
verdict shapes is a feedback loop, since Sell-as-exit is near complementary
to Buy and would close the very gap that produced it. The gap's SE now leans
conservative rather than anti-conservative for the same reason.

LIVE. The retargeted class is wired to close the position, or training it
would be pointless: CheckClosePosition's "never vote-exit a certified
position" rule keeps governing symmetric books and gains a one-sided
exception, and the replay reads the identical rule through one
LiveVoteExitThreshold() so certified and traded cannot describe different
policies. Armed only when the operator picks a close threshold
(Signal_ThresholdClose ships Disabled) AND the model's own pin says its
blocked-side class means "close" - a model trained symmetric never fires it,
whatever the verdict has since become. This does trade a different game from
the one the win-rate certificate grades; the era's EXIT-POLICY REPLAY line
already reports expectancy in R for exactly this case and says so in words.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:02:50 -04:00