Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
~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>
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>
Use_Training_Pool gates a fully-built, fully-wired mechanism
(Expert\Training\TrainingPool.mqh + the Add/Adopt/Publish call sites
already in Training.mqh) that shipped false. Nothing to build - the
writer/reader/atomic-file/compat-gate/age-gate/lookahead-purge were
all already there, measured +2.02pp of paired skill at H4 (research/
edge.py, 2026-08-24). Flipping the default is the whole change.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two chart-display fixes reported after watching a converged 4-model
ensemble: the ensemble panel's trailing "(era 69, 4 models,
DEPLOYING)" was frozen at whatever era the ensemble happened to
deploy on, and the separate top-right HUD (one line per model, raw
B/S/N + weight + era + error) was clutter once the vote itself is
what matters.
Root cause of the freeze: g_ensembleVoteLine is written once per era,
at pass-3 completion. A deployed/converged ensemble runs no further
eras (ScheduleTrainingIfNeeded's trainingComplete branch skips
Train() entirely), so that line could never update again - the era
count and "DEPLOYING" marker were permanent set-dressing from the
deploying era, not a live reading.
- EnsembleScoreCombinedVote() drops the era/DEPLOYING tail once
g_ensDeployApproved - nothing left there worth freezing.
- UpdateVoteReadout() (the aggregate "VOTE ..." line, previously its
own top-right chart object) now writes g_liveVoteLine instead of
drawing anything. Both status-label builders - PublishEnsembleStatus
for the ensemble panel, PublishStatus's choke point for the solo
panel - append it as one line, refreshed every tick/timer exactly
as the old HUD was, so the live vote replaces the frozen era tail
in the same visual slot.
- RefreshVoteReadout()'s per-member loop (DisplayHudLine, one
ObjectLabel per model) is deleted outright rather than folded in -
the operator asked for the aggregate only, "without telling me each
individual network".
Follow-on dead-code removal, since DisplayHudLine was the only
caller: the DispProb/DispSignal/MetaGateArmedNow/MetaHasScore/
MetaLastP/MetaLastBe/MetaApproved/MetaVetoed leg of IChartView (and
its AIBaseChartView/AIBaseChartViewImpl/ExpertSignalAIBase forwards)
had no other reader. The underlying data survives untouched -
m_metaTelemetry is still populated live by SignalMETA.mqh,
m_dispSignal still feeds ProspectiveVote - only the chart-view
forwarding that existed solely to reach the deleted HUD is gone.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DIRECTION_INTELLIGENT and the drift verdict it fed were removed in
the step-3 demolition (8f21646); WarriorDirectionAllows() now
resolves purely from tradingdirection (LONG_ONLY/SHORT_ONLY/BOTH).
Two comments in the OOS-verdict certification path and the filtered-
overlay reconstruction still described the deleted mechanism -
found while auditing both paths for correctness. No behavior change.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The capacity budget is stated in weights per INDEPENDENT observation
and divides by the mean label lifespan to get there. It never once
did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is
only ever called from InitNeuralNetwork, where the label cache does
not exist yet (that same function sets m_labelCachePrebuilt = false
a few lines below), so MeanLifespan() returned its "nothing measured"
default of 1.0 at every call. Every fresh model was sized as though
its labels did not overlap - over-budgeting the first dense layer by
a factor of L, which is several rungs of a power-of-two ladder. The
"expect overfitting, reduce the feature set or pool instruments"
warning is the branch that should fire on H1 and structurally could
not.
Fixed at the source rather than by reordering the boot sequence (the
prebuild is chunked across Train() calls and cannot complete inside
init): MeasureSwingGeometry() walks the ZigZag ONCE at init and
answers both questions from it - the median leg gives the window,
and the leg series gives the mean label lifespan analytically.
SwingPivotDirectionLabel resolves bar i when the SECOND pivot after
it commits, so a bar d bars before pivot P waits d + (the leg
leaving P); summed over every bar of every leg that is exactly the
mean the label walk accumulates.
That also closes the coherence gap the swing target opened: the
window was measured with a private +/-12-bar fractal while the label
aimed at ZigZag(12,5,3) pivots, so it was sized against a leg
distribution the label never used. One pivot source now, the
label's.
Also:
- ResetWeights() re-derives the shape. It rebuilt from the members a
history-starved init had pinned and re-saved them - so the "let
history download, then reset from the panel" advice in both
fallback warnings did nothing at all.
- The CAPACITY line prints the measured lifespan beside the one the
topology was sized for, and warns when they differ by more than a
ladder rung. That is the check that makes the estimator falsifiable.
- Topology reads the view's symbol, not _Symbol (latent for pooling).
- Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0:
under-sizing is recoverable, over-sizing silently is not.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
- 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>
Operator's observation, verified against Examples/ZigZag.mq5's selection loop:
the only erasures it performs are ZigZagBuffer[last_high_pos] while hunting a
bottom and ZigZagBuffer[last_low_pos] while hunting a peak. A pivot therefore
leaves the erasable slot permanently the moment the OPPOSITE pivot is committed,
and can never move again - the opposite pivot does not itself need to be final.
SwingPivotDirectionLabel now waits for that event instead of for
m_swingConfirmationBars. The bar aims at P1, so it becomes trainable once P2
exists; pivots alternate by construction, so P2 is the next non-zero bar and
needs no type test. Until then the label is not knowable and the bar is Neutral.
Exact rather than a guess, and it removes the need to measure a repaint-lag
distribution at all. SwingConfirmationBars keeps its other uses; it is no longer
this target's lookahead control.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Series indices are relative to now, so one new bar moves every cached bar's
index by one. EnsureBarCachesCapacity answered that by wiping the label cache,
the excursion caches, the ladder and the feature cache and rebuilding the whole
prebuild from scratch - on any timeframe where a bar closes before a run
finishes, the labels were being recomputed continuously and the training set
never held still.
The labels do not change when a candle closes. ShiftBarCaches moves every
per-bar cache up by the number of new bars, marks only those newest bars as
unfilled, and leaves the rest exactly as computed. CFirstPassageLadder gets a
matching Shift (resizing directly rather than through Allocate, which zeroes the
ages this is preserving).
Refuses, falling back to the full rebuild, when a prebuild is mid-flight: its
cursor is an index into the array being moved.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TARGET_SWING: the direction models learn which way the next CONFIRMED SWING
PIVOT lies from the current close. Geometry-free - the label owes nothing to a
stop, target or horizon - which is what lets trade management be tuned
separately instead of being baked into what the net learns.
SwingPivotDirectionLabel reuses the ZigZag pivot the horizon and leg-size
measurement already walk, so there is ONE notion of "pivot" in the codebase. It
walks forward in time and stops at m_swingConfirmationBars: a pivot nearer than
that is still repainting, so its label is not knowable yet and the bar stays
Neutral. That boundary is the whole lookahead control for this target.
TrainingTarget input is back (TARGET_BARRIER default, unchanged behaviour) with
TARGET_FRACTAL and TARGET_SWING beside it; |TGT:SWG1 joins the fingerprint so
switching trains a separate model rather than relabelling an existing one.
ADZigZag was renamed to ZigZag throughout (30 identifiers). It has loaded
MetaTrader's stock Examples\ZigZag at its stock defaults for some time - the
migration was done, only the name was left behind, and a name that says "AD"
about a stock indicator is exactly the legacy pointer this codebase should not
carry. No behaviour change: same #resource, same params.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First 35 eras across both charts, this run:
shipped 1.21/2.43 (SP500) and 1.26/2.52 (USDJPY): mean -0.0525R,
positive in 6 of 35 eras
best plateau after the neighbourhood guard: mean +0.0292R,
positive in only 17 of 35
most-recommended pair: 20.00/0.50, seven times - a ~40:1 lottery that is
simply the least negative cell in an all-negative grid
The recommendation jumps between opposite corners of the ladder between
consecutive eras, which is a grid fitting noise rather than a geometry worth
adopting. Two changes so the line cannot be misread:
- GEOSWEEP_MAX_TIMEOUT_SHARE (0.70): a cell where most trades never touch
EITHER barrier is not a geometry being tested, it is the horizon close being
measured. 20.00/20.00 timed out on 100% of trades and was still selected.
Excluded from SELECTION only; the cell stays filled and readable.
- When the winning plateau is <= 0 the line now says so in those words:
"NOTHING ON THE LADDER PAYS ... the pair below is the LEAST NEGATIVE cell,
not an edge."
Still measurement only - nothing reads the recommendation and no geometry moves.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
selectionScore used to be a win rate in percentage points and printed at one
decimal everywhere. Under DeployOnExpectancy it is expected value in R, so
"%.1f" rendered every real score as "0.0" - era 2's +0.05R and a genuine zero
looked identical, which makes the journal useless for watching the ranking the
plateau ladder is doing.
One formatter, DeployScoreText(), next to the score it formats: "%.3fR" under
expectancy, "%.1f%%" under significance. Routed all nine print sites through it
(ensemble era line, best-so-far, panel, regression, new-best, era-cap prompts,
the convergence line, the deploy dialog) and dropped the "%" suffixes they had
hardcoded. No new prints, no new log lines.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TWO CHANGES, both of which turn a permanent "nothing happens" into a decision.
1. THE DEPLOY GATE ASKS THE WRONG QUESTION. tradeable required the win rate to
clear chance by EDGE_MIN_SIGMAS - "can I PROVE an edge exists" from one OOS
window. On H4 that asks ~66% against a market supplying ~53%, so it is
unreachable by construction and no run has ever deployed through it.
SDeployVerdict now also carries the economics of the geometry actually being
traded - cost-adjusted break-even and reward:risk, both from the new
CostAdjustedGeometry() so a spread convention cannot be applied to one and
missed on the other - and derives
E[R] = (p - p*) * (1 + RR)
which is exactly zero at break-even by construction, so "profitable" and
"beats break-even" can never disagree. Under DeployOnExpectancy (new input,
default ON) tradeable becomes E[R] > 0 and selectionScore ranks eras by
expectancy instead of precision. Coverage and both-sides-live still gate
both: an expectancy over a handful of one-sided calls is not tradeable.
The struct also publishes scoreSE - the SE of selectionScore IN THE SCORE'S
OWN UNITS - because the score changes units with the objective (win-rate
points vs R). Both plateau bands now read it instead of precSE, which was
right for one objective and dimensionally wrong for the other.
Setting DeployOnExpectancy=false restores the previous behaviour exactly.
2. THE FILTERED VIEW COULD NOT DRAW WHILE ANY MODEL WAS TRAINING.
HistoricalNetVote built its divisor from VoteCapableWeight(), which answers
"may this member move real money" and returns 0.0 for an AI member until the
whole run converges. So the reconstruction's divisor was zero on EVERY bar,
every bar was skipped as "nobody looked", and the chart drew nothing at all -
for the entire training run, which before the plateau noise band was forever.
Reported as "no signals drawn since the refactor".
New ReconstructionWeight(): the same weight WITHOUT the converged-run
requirement, overridden on the AI member to ModuleWeight() gated on
SelfRanked() only. The overlay is a picture of what the vote WOULD have
shown, which a mid-training model can answer - the chart HUD already says so
with its "(trn)" marker. Live Direction() still uses VoteCapableWeight(), so
no untrained model gains a say in an order.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every era was a ~1,200-bar chunk of a 16,264-bar window, and the oldest 90% of
the history was never reached.
All four passes yield mid-chunk on the 120ms budget: each one calls
StashEraResume (the single writer of m_eraResumePending) and returns. Those
used to be returns from Train() itself. When the passes were extracted into
their own methods (08c2cec) they became returns from a void helper, and Train()
carried straight on - reporting pass 1 "done" after one budget, running pass 2
over the sliver pass 1 had queued so far, scoring an OOS slice of it, and
letting AdvanceEra count an era. The extraction moved one side of the binding
and left the reader behind.
Measured on SP500 H4 (VerboseMode, 2026-08-24 15:05-15:14):
era 0 TRAINING WINDOW = 16264 bars ... Bars(series) = 16264 <- window fine
era 1277 pass 1 done in 0s - 1144 of 1193 bars usable <- sweep is not
era 1296 pass 1 done in 0s - 3117 of 3166 bars usable
era 1318 pass 1 done in 0s - 1391 of 1440 bars usable
~1,400 eras in ten minutes, the count varying with how many bars a 120ms budget
happened to buy. Downstream: each member held a different tiny OOS slice, so
the combined vote's shared-bar intersection collapsed ("0 shared OOS bars" on
nearly every era, score 0.0), and the plateau ladder counted 46 ungraded eras
as a plateau and fired a boosted warm restart on all four models.
Train() now returns whenever m_eraResumePending is set - after pass 1 (before
ReportPass1Outcome, which has no verdict to give on a yielded sweep), pass 2,
the calibration walk and pass 3. m_modelEta is already saved inside
StashEraResume, so the early returns keep the learning-rate trajectory.
The resume machinery itself was correct and is unchanged: BeginEra's resume arm
restores the cursor, m_passWindowOk/m_passWindowFail accumulate across chunks,
and the m_isPass2Active/m_isPass2Done guard already routes a resumed call to
the right pass.
Expect era numbers to advance slowly now. That is the fix, not a new stall.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Two charts (SP500 H4 + USDJPY H4) ran with the pool enabled and produced no
TrainPool directory, no adopted rows and not one journal line. The pool was
inert and there was no way to tell that from "the feature is off".
It could never have fired: the fingerprint is not symbol-invariant. It hashes
NeuronsCount, which counts the alt-data columns - and those are per-symbol
(SP500 carries cot_spec_net, the FX majors cot_idx_1y/3y/chg_4w) - and the
cross-asset block appends ":IDX2" when base currency == profit currency, true
of an index and false of a pair. SP500 came out 50 features wide under
XA:6:IDX2, USDJPY 52 wide under XA:6. Compatible() gates on both, so adoption
was zero by construction.
- STrainPoolHeader::MismatchReason() replaces the bare Compatible() predicate
and names the mismatch; Compatible() now delegates to it, so "may I adopt"
and "why not" can never drift apart.
- CTrainPoolReader::Adopt() reports its own verdict - adopted, alone, or every
peer rejected with the reason per file - and reports it on CHANGE only. An
era over a warm feature cache runs in a fraction of a second here, so a
per-era line would bury the journal. The duplicate Print in RunPass2 is gone;
pool state is now reported from exactly one place.
- CTrainPoolWriter::Publish() rate-limits to TRAINPOOL_MIN_PUBLISH_SEC (300s).
Every era re-derives the same rows from the same in-sample span, so per-era
publishing rewrote a multi-megabyte file continuously for no new information.
The first publish is never delayed.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Peer rows join m_isTrainQueue as NEGATIVE sentinels before the shuffle, so they interleave with
this chart's samples instead of training in a block at one end. A block would be a curriculum:
whatever the optimizer saw last would decide where it landed.
TrainPoolStep is a separate path on purpose. Everything in pass 2's local branch after the
forward pass reaches for something indexed by a LOCAL bar - m_labelCache, m_winLongCache, the
excursion target, the arrow cache, m_Time - and a peer row has none of those. Sharing the path
would mean inventing values for all of them, which is how another instrument's outcomes end up
inside m_cumIsCorrect and the operating point gets fitted to them. The IS-vs-OOS gap is read as
THE overfitting signal, so polluting the IS side would not crash anything; it would just quietly
stop meaning what it says.
The purge key reuses the label walk's own two bounds - the horizon and NextScheduledCloseAll -
rather than approximating with a bar offset. A second horizon model here would drift from the
real one, and this project already measured that the close-all, not the nominal horizon, is what
actually terminates labels. Cutoff is the OLDEST OOS BAR'S TIME, in wall clock, because bar
indices cannot be compared across instruments that each have their own calendar.
Contribution happens while the window is still in TempData and before the forward pass
overwrites it, and is gated to direction models: the meta head trains a different target on a
wider input, which the fingerprint gate alone would NOT catch, since a meta model's fingerprint
matches its own peers perfectly well.
Use_Training_Pool ships false and does nothing until a second chart runs a matching fingerprint.
Compile-verified against a BASELINE of the same tree without the wiring: both produce 12
errors, all error 313 invalid-resource-path from #resource directives that cannot resolve in a
headless staged build (stock Controls res\*.bmp, plus the pre-existing Network.cl). Code errors
0, warnings 0, identical to baseline. Staging copy and junctions removed; the live .ex5 was
never touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PooledGate pools the DECISION; this pools the DATA. Measured in research/edge.py with both arms
sharing calendar folds, exit-time purge, benchmark and scoring so training breadth is the only
variable: H4 k=2 gap +2.02pp at t_mkt 3.97, which CLEARS the Sidak bar of 3.69 over five feature
sets at df=6, replicated independently at D1 k=1 (+2.03pp, t_mkt 2.79). The per-instrument arm
was NEGATIVE on every feature set at both timeframes - it loses to "always take the drift side".
This EA trains one net per chart, which is that arm.
Rows, not symbols. Pointing the feature stack at another symbol needs per-symbol indicator
handles and this project has been bitten there twice - the handle leak that never released the
old handle, and the twelve "dead" handles that were one shared refcounted iMA. Each chart
instead computes its own features with its own handles and shares the NUMBERS. Sound only
because FeatureBuilder already ATR-normalises every price-unit feature, for exactly this reason
("instead of feeding e.g. 0.0005 on EURUSD").
Not a fingerprint participant: pooling changes what the model is trained ON, not what it IS, so
adding it would re-key every .nnw to record something outside the model's identity. The
fingerprint instead GATES adoption - it is the assertion that column k means the same thing in
both files - alongside a width check (a fingerprint match with a width mismatch means one side
pinned an older layout) and an exit-TIME purge, since a bar index cannot be compared across
instruments that each have their own calendar.
Writer and reader are separate classes: different reasons to change, different lifecycles, and
one class would carry the export buffers through every read. The file layout lives in one
STrainPoolHeader used by both sides so a layout change cannot be applied to the writer and
missed in the reader. Staging goes through System\AtomicFile rather than a second hand-rolled
temp-and-rename.
Compile-verified in isolation: 0 errors, 0 warnings. Staging junctions and harness removed; the
deployed .ex5 was never touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The verdict recorded in this docstring - that volume, time and alt all land at +0.019-0.020,
identical to price alone, so none was being used - came from a run whose weekend clock was a
day early and keyed on a bar most feeds never trade. That left two feeds with 74-78% unresolved
trades and sd(R) of 0.21 against everyone else's 0.62, which handed them overwhelming weight in
the inverse-variance pooling.
With the clock fixed the ordering inverts. `geom` becomes the WORST row rather than the
equal-best one, and price+time nearly doubles it:
price+time +0.047 | price+vol +0.038 | price +0.037 | ALL +0.028 | price+alt +0.027 |
geom +0.026
So price and time DO add ranking power over the strategy's own entry arithmetic. What survives
both versions is the alt result: every set containing alt columns scores below the same set
without them, agreeing with the direction screens at H4 and D1.
`shuffle` permutes the training outcomes while leaving fold boundaries, purge, threshold rule,
kept fraction and scoring identical. A lift that survives that comes from the machinery, not
the data - and this session has already produced two results that did exactly that, so the
+0.047 does not get believed until this run comes back near zero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The encoder was rewritten once to convert every value rather than the one field known to hold
an array. It still only looked one level deep, and `evaluate` attaches the ENTIRE built matrix
under a 'd' key - so it stepped past d['X'] and killed the last line of a second 40-minute run
with the same TypeError the first fix was meant to end.
plain() now recurses into dicts and lists. The matrix and its column index are dropped rather
than converted: they are working state, tens of MB per instrument, reconstructible from
features.build, and nothing re-analysing these rows needs them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The portfolio case for this strategy is that more uncorrelated strategies alongside it beat
an index. That only holds if the members do not share a hidden common factor, and a backtest
correlation matrix cannot see one: it is dominated by the calm months that make up most of a
sample, while the shared exposure surfaces in the month that breaches a drawdown limit.
So measure it directly. Aggregate each strategy's R by month, correlate against the
underlying's own monthly return, and split into up-months and down-months where a long bias
actually shows.
The answer is not marginal: mean correlation +0.69, positive on 14/14 feeds across crypto,
indices, energy, FX and metals, R2 up to 0.62 on USDJPY. Mean monthly R is +1.94 in up months
against -1.61 in down months. Every vendor pair agrees to within 0.03. These are not seven
independent bets, they are one bet placed seven times.
The placebo decomposition says why: across eight markets the barrier term is close to the
negative of the drift term (BTCUSD +0.055/-0.041, SP500_d +0.083/-0.030, USDCAD_d
-0.056/+0.059). The stop and target are a trend-capping device - they clip the gain where the
asset rises and limit the loss where it falls - so the residual cannot be an edge. It is the
same exposure with both tails trimmed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two-arm version answered only half the question. It showed the MACD entry beaten by random
timing on every feed, but left the residual +0.01 to +0.05 R unexplained, and the first story
built on it - that the strategy is a drift harvester - died on the second market.
The drift arm settles it by running the same random entries with the stop and target moved out
of reach, so every trade holds to the weekly close. Its R is then rebuilt by hand on the REAL
stop distance, because simulate divided by the widened one; leaving that alone would report
every drift trade as ~0 R and make the comparison vacuous. real-timing is what the signal is
worth, timing-drift is what the barriers are worth over just holding.
The driver moves into this module as a `placebo` subcommand instead of living as a loose script
beside it, and reports per-market statistics for both terms.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was reported in every table as the drift this strategy inherits, and as the column that
isolates what the RULES contribute. It is neither. buy_and_hold_R marks each trade at
exit_idx - the bar its own barrier fired on - so a trade that exits at its target is compared
against the close of the bar that touched the target. The two are nearly the same number by
construction.
Measured, the per-trade difference has sd 0.10 against R's own 0.62. That is where a
per-market t of 8.23 and p=0.00017 came from: a quantity that mostly cannot vary will always
look significant. Every "skill over buy-and-hold" figure quoted from this module is withdrawn.
What the column legitimately shows is exit slippage, and it now says so.
placebo() replaces it. Same number of entries, same previous-day-low level, same ATR-scaled
stop and target read at the entry bar, same weekly close, same non-overlap, same fill engine -
only WHEN the orders are placed moves, drawn from the bars the rule could have fired on so the
null inherits the same calendar exposure. Geometry then appears in both arms and cancels, and
only the MACD timing is on trial, which is the question that was being asked all along.
The run also now prints whether any vendor PAIR disagrees in sign. Before the weekend-clock
fix SP500_d and SP500_5 disagreed (-0.003 against +0.064) and so did the two FTSE feeds; they
are the same market at correlation >= 0.999986, so that disagreement was evidence of a
machinery fault and nothing noticed it. Now it cannot pass unremarked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs in one function, and together they invalidate every number this module has printed.
1. `(days + 4) % 7` makes Friday 5 and THURSDAY 4, so every `dow == 4` test matched Thursday.
Trades were force-closed a day early and the no-entry window blocked Thursday night to
Saturday night. Epoch day 0 is a Thursday, so Monday=0 needs +3. Verified against a known
calendar week instead of re-derived by argument.
2. The close was keyed on a literal 23:45 stamp. That assumes every feed trades up to it and
they do not - FTSE_d has 175 such bars in its entire history against XAUUSD_d's 13,657,
because an index CFD session closes hours earlier. Most FTSE trades found no close ahead of
them, and a trade past the last close bar got a negative horizon that maximum(_, 1) turned
into a ONE-BAR hold: a silent instant exit indistinguishable from an ordinary unresolved
trade. The close is now the last bar of the trading week, which is feed-agnostic and is
what 'flat for the weekend' means.
The tell was in the diagnostics, not the result: SP500_d and FTSE_d showed 74-78% unresolved,
sd(R) of 0.19-0.21 and 1.1-hour holds while every other feed sat near 0.62 and 20 hours. Those
two carried a third of all trades and, having almost no variance, dominated the inverse-
variance pooling - which is where metafilter's implausible t_mkt of 9 came from.
Corrected, the five feeds agree: 22-34% unresolved, sd(R) 0.61-0.64, RR 0.25-0.36, win 67-74%,
and expR still positive on all of them at 1bp/side. The finding survives; its statistics do not
and are being re-run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was introduced as 'no market information whatsoever'. That was wrong. With g = fill minus
the previous day's low, rr = (6.51*ATR25 - g) / (g + 3.23*ATR15), a monotone decreasing
function of g/ATR - so rr is a NORMALISED DISTANCE ABOVE YESTERDAY'S LOW, a price feature in
the same family as donch and smadist, reparameterised until it looked like bookkeeping.
What survives the correction is the part that matters: volume, time and alt add nothing, every
combination lands at +0.019 to +0.020, and price alone already reaches +0.020. What changes is
the explanation - the lift is one price relationship, not an absence of one.
And the relationship is not monotone, so 'prefer a better payoff ratio' is the wrong summary.
By decile on SP500, XAUUSD and USDJPY alike it is an inverted U: filling far above the low
pays ~0, the middle band (rr 0.13-0.40) pays +0.05 to +0.14, and filling AT the low is
negative on all three. Buying the level the strategy aims at is the losing case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First run returned +0.020 R lift at t_mkt 6.5 - an order of magnitude beyond anything else in
this project - and the tell was in the same table: price, price+alt and ALL returned the SAME
lift to three decimals. A model given more information that does exactly as well as one given
less is not using the extra information, so whatever it found was in something all five sets
shared.
`geom` is that something: two columns of the strategy's own entry arithmetic, the realised
reward-to-risk ratio and risk as a fraction of price, both known at entry and carrying no
market information at all. It scores +0.022 - the LARGEST lift in the table - and ALL+geom at
+0.020 is no better. Every data family contributed nothing, which is exactly why they all
agreed.
The cause is the entry. A buy stop at the previous day's low sits below the market, so it
fills a median 4.75 ATR from the level its stop was sized against and the reward:risk of each
trade is close to arbitrary. The model was ranking that, not the market.
Also adds cut_from='train'. The keep-threshold was taken from the TEST fold's own prediction
quantile, which keeps exactly q by construction but cannot be known in advance - so the filter
as first measured was not implementable. The training quantile is fixed before the fold is
seen and lets the kept fraction float, which is the version that could be traded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
edge.py asks a question that is mostly closed in this project: can a model call direction on
a symmetric barrier. Filtering is a different and easier question - the rules have already
chosen the side, and the model only has to rank trades that were going to be taken. A series
that cannot say 'up or down' can still say 'not today', and nothing built here so far could
have detected that.
For each trade the strategy takes, the feature vector is read ONE BAR BEFORE the decision bar
- every column in features.py is a function of bars <= i including i's own close, and the
order goes in at bar i's open, so reading row i would hand the filter the outcome of the bar
it is deciding on. A gradient-boosted regressor predicts R under a purged walk-forward, the
top q of each test fold is kept, and the lift is measured against the mean R of ALL trades in
those same folds.
That baseline is the one that cannot be gamed by the strategy being good: a random subset of
the same size has expected mean equal to the fold mean, so the difference is exactly what the
ranking contributed, and a profitable strategy raises both columns together rather than the
gap. Families are ablated as in edge.py and the verdict is read from the per-market t.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A generated strategy is not the shape edge.py measures. That screen asks whether direction is
callable on a SYMMETRIC barrier; this one is long-only with a target near twice its stop, so
it can pay at a win rate well under 50%. 'Direction is at chance' and 'this makes money' are
not in conflict - they are different measurements, and the way to settle which applies is to
replay the rules rather than argue from the screen.
Both readings of the entry are implemented because they are not the same strategy. Taken
literally, a BUY STOP at the previous day's LOW sits below the market, triggers at once and
fills a median 4.2 ATR above its own level - 91% of the time - so the stop loss is measured
from a level the trade never touched and realised reward:risk lands at 0.28 rather than the
~2.0 the coefficients imply. Read as a pullback (LIMIT), the geometry comes out at 2.1 as
designed and only 40-60% of orders ever fill. A trade export decides which one the generator
ran; nothing else can.
Also carries a correction. The docstring first claimed the omitted trailing stop could not
bind because activation sat far out. Measured, activation is at 0.54R - it arms before the
trade is one unit of risk in profit. The claim was wrong, the number is now printed every
run, and the omission is recorded as the largest deviation rather than a small one.
edge.save_rows writes to a temp file and renames. The first D1 run crashed mid-dump and left
a truncated JSON at the canonical path, which is worse than no file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every pooled statistic here was reported as a bracket: SE_INDEP, which claims 28 cells are 28
independent tests, and SE_CORR, which claims they are one. Neither is the number. The 15 feeds
are 7 markets - six PAIRS entries are one instrument quoted by two vendors at correlation
>= 0.999986, and ES_fut, SPY_d1, SP500_d and SP500_5 are four claims on the same index - so
market_t() collapses each market's cells by inverse variance and takes a plain t across the
market means. Its degrees of freedom are separate price series, which is the only n this
catalog can defend, and it is now the column the verdict is read from.
catalog.MARKET is where that collapse lives, next to PAIRS, because it is the same fact.
save_rows() writes both arms' scored cells, per-trade diff vectors included, to a JSON beside
the bar cache. The screens cost ~40 minutes and produced nothing but a printed table, so
re-pooling, collapsing feeds, or reweighting a threshold meant paying for every fit again -
which is why the H4 run's rows are gone and it has to be re-run to get them back.
Also folds two spellings of the Market Data root into catalog.ROOT; sqxbars had its own copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six bare literals across four functions all encoded the same judgement - below this many
rows, a fit or a score is noise - and all six were absolute counts tuned on H4. D1 has six
times fewer bars per year, so a D1 run would have dropped almost every instrument from the
sample without saying it had: build wanted 3,000 labelled rows and no D1 series but SPY has
that many, and evaluate wanted 5,000, which nothing has. The run would still have printed a
table, just a much emptier one, and the emptiness is exactly the kind of thing that reads as
a null result.
floors_for(tf) scales them by bars-per-year so the judgement stays 'this many YEARS', with
clamps so the coarse end cannot scale down into a sample no statistic survives. FLOORS is
set once at the entry point, after the timeframe is known, and printed with the run.
k and the barrier window join it as arguments. k was pinned at 2.0 in three places while the
banner claimed 'k=2' unconditionally; at D1 that same k resolves in ~11 days and leaves ~250
independent trades per instrument, which is a different experiment from the H4 one and needs
to be requested rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The screen now runs two arms that differ in exactly one thing - what the model was
trained on - and compares them cell by cell.
THREE CORRECTIONS, each of which biased toward finding an edge:
The pooled arm's STATIC BENCHMARK was fitted globally on all instruments' training
rows. Any instrument drifting against the pool got an "always long" benchmark while
it was actually falling, handing the model a win it had not earned. Now fitted per
instrument on that instrument's own purged rows.
The two arms used DIFFERENT FOLD BOUNDARIES - the pooled arm cut on calendar time
(it must: the instruments have different bar counts and start dates), the solo arm
on row index. So any difference between them mixed "pooled training helps" with
"the arms saw different years". Both now share calendar folds, and the solo purge
moved from bar index to exit TIME to match.
The pooled model was REFIT PER THRESHOLD. The fit does not depend on the threshold,
only the walk does, so this doubled the cost of the most expensive arm for an
identical model. One fit now serves all thresholds.
Dropped the EMBARGO constant: purging on exit time already keeps a training row only
if its trade had closed before the test window opened, and a bar-count embargo on top
is a second, weaker statement of the same rule.
RESULT, on identical columns, folds, purge, benchmark and scoring:
price pooled +1.18pp per-instrument -0.62pp
price+vol pooled +1.33pp per-instrument -0.83pp
Per-instrument training is NEGATIVE on every feature set. Nothing clears a defensible
bar in either arm - the best of 140 cells reads t=2.79 against a Sidak bar of 3.56,
which is what the maximum of a null grid looks like - but the GAP between the arms is
the largest effect in the run, and the EA trains a net per chart.
Also: alt data is negative in every alt-containing cell of both arms. Six macro
columns against ~1,900 independent trades buys overfitting, not information.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
features.py assembles one causal matrix per instrument - 18 price columns in ATR
units, 4 volume ratios, 5 time encodings and the alt block (COT positioning, VIX,
curve, dollar, breakevens, Fed funds, and the instrument's own implied vol where one
exists) - plus a symmetric k*ATR first-passage label. edge.py evaluates it.
Four things it does that the naive version does not, each of which has already cost
this project a retracted result:
BENCHMARK Gold's up-first rate is 53.49% over 23 years against a 51.00%
break-even, so "go long" alone looks profitable. Skill is the excess
over the best STATIC side fitted on the training fold, and its t is
PAIRED - the static walk takes the same trades, so drift, regime and
sample composition cancel. An unpaired t against break-even is the
drift's t, not the model's.
PURGING Barrier labels stay open for many bars, so training rows whose trade
exits after the test window opens are dropped, plus an embargo. In the
pooled arm the purge is on TIME, not bar index - the instruments have
different calendars and index-purging would align 2015 with 2021.
INDEPENDENCE The scorer walks each fold sequentially - take a signal, jump to that
trade's exit, look for the next - so it counts what an account could
have taken instead of counting the same swing once per bar.
NULL A rotation null is provided for the best-of-N problem: rotating the
fitted predictions against the labels keeps both series'
autocorrelation and destroys only their alignment.
Two design errors found and fixed while building it, both worth keeping visible:
The alt `_na` missing-flags were a DATE PROXY - each flips once at its series'
first publication, so a tree reads "before 2010" and fits that era separately.
`alt_mode='restrict'` (now the default) keeps only the published era and carries no
flags. It matters: gold's price-only cell went from t=3.55 to t=0.35 under it, so
that apparent edge lived entirely in the pre-2010 sample.
Coverage was reported as trades/bars, which reads 8% for a walk that is actually
taking ~90% of every slot available. Non-overlapping trades make the ceiling
bars/duration, and the honest number says there is nothing left to take.
Also fixes a regression this change introduced: `book._resample` divided the summed
spread by `v` to get the mean, which was correct only while `v` was a bar COUNT.
Volume is now real (carried from the .dat's sixth field rather than discarded), so
that divisor is now the bar count explicitly - USDJPY H4 reads 0.640 bp against the
catalogued 0.64. Frame.has_volume distinguishes real volume from a bar-count proxy,
because a "volume feature" built on the latter is measuring session length.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first version reported `required / floor` (the spread's win-rate hurdle over the
smallest uplift the sample can resolve) and called a LARGE ratio cost-bound,
concluding H1 was untradeable on all 14 instruments. That inverts the meaning. A
large ratio means the hurdle sits many standard errors away, so a break-even-sized
edge would be seen at overwhelming significance - USDJPY H1 read 5.89, which is a
break-even edge showing at ~12 sigma. That is a well-POWERED cell. The bad case is a
SMALL ratio: cost cheap, nothing measurable.
The tell was in the same row and went unchecked: it also said the spread was 4.5% of
a 1-ATR stop and break-even was 52.25%. Neither supports "cannot pay its spread".
When a derived ratio disagrees with the raw quantity it came from, the raw quantity
wins.
Rebuilt around the number that actually decides whether to act:
CONFIRM = 50 + 50*costR + 2*SE
the win rate a symmetric kR:kR setup must hit to be PROVABLY profitable. The two
terms pull opposite ways in trade size - widen the stop and the spread shrinks as a
share of the move, but each trade eats more history so SE rises - so CONFIRM is
U-shaped and its minimum is the cell worth testing first. This also makes explicit
that the horizon axis and the stop-multiple axis are the same axis: a 20-bar hold on
H1 is an H4 trade, and the table now prices both.
Durations are MEASURED by walking each trade to its first barrier touch rather than
assumed to follow the diffusive k^2 scaling, which is off by an instrument-dependent
factor. Trades open at the window cap are reported, since they make n_eff optimistic.
Result on the four longest histories: the best cell needs 51.9-52.9% and the surface
is FLAT from H1 k=2 to H4 k=2. There is no magic horizon. Add commission and the
working target is ~53% - against a deploy gate that asks ~66% at 18% coverage purely
because its OOS window holds ~63 independent observations. The binding constraint is
the gate's window, not the market.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It listed SignalStoch/SignalPB/SignalITF/SignalRVI and six other files as though
present, marked SignalSessionFilter '(Removed)' while its 207 lines were being
instantiated on every init, and omitted SignalMETA (578 lines), OscillatorDivergence,
SignalRiskGuard and SignalHYBRID entirely - so the one file meant to orient a reader
was wrong in both directions at once.
Rewritten against the actual roster, with the removal reasons recorded here rather
than in commits nobody re-reads, and with the two conventions that are easy to get
wrong on first contact: the voting machinery is stock CExpertSignal (so 'simplify
back to stdlib' removes nothing), and Direction() is a transaction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
research/classic.py transcribed all 26 shipped vote patterns (MA 4, RSI 4, MACD 6,
Ichimoku 12) with their constructor weights and tested them as entries on 178k-bar
histories, four instruments x three barrier geometries. Nothing separated from
chance - not one pattern, not the averaged vote at any threshold 10-70, not a
2/3/4-module quorum, not event-plus-confirmation. Residual E[R] everywhere was
-0.01 to -0.08 R, which is approximately the spread. The +4 sigma reading that had
once justified the set was two bars of lookahead: closing it took MACD_p4 on EURUSD
from +5.05pp to -0.02pp.
All four inputs have shipped false ever since, so this deletes dormant code rather
than changing behaviour.
RETRAIN-NEUTRAL, deliberately. EnableMA and EnableRSI were hashed UNCONDITIONALLY
into the DB config fingerprint, so they become literal 0 legacy slots - the same
treatment the ind_Periods slot two lines above already uses, and every existing
database keeps its key. EnableMACD/EnableIchimoku were appended only when enabled,
so with both gone the segment simply never appears, which is byte-identical to
today. No .nnw or .db is orphaned.
WHAT THIS COSTS, STATED PLAINLY: these four were CSignalMETA's only wired candidate
sources, so the on-chart ladder sweep (BuildCorpusBySweep) now has nothing to sweep
and a META chart is no longer self-contained. That is survivable rather than fatal
because MetaPrepareEra already falls back to CMetaCorpus::LoadLargestOnDisk, and its
own comment names this exact case - "charts whose classic filters are disabled".
Use_MetaLabeling ships false regardless. SignalMETA.mqh is otherwise UNTOUCHED, and
its 26-slot one-hot stays at 26: a tester-built corpus on disk still encodes those
pattern ids, and narrowing the descriptor would invalidate every stored corpus.
Signals/SignalMA.mqh SignalRSI.mqh SignalMACD.mqh SignalIchimoku.mqh deleted
Signals/OscillatorDivergence.mqh deleted - RSI and MACD were its only users
Classic_Shift deleted - the four votes were its only readers
Compile-verified in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline
taken before any edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tick .dat files and the bidask/bars/sqxbars caches are gone, so every screen
that reached for fills.Book was dead and breadth.py's five symbol keys no longer
matched the SQX export (it has been rewritten with ONE underscore, turning every
lookup into FileNotFoundError). Rebuilt the data path from the only source left -
the SQX bar files - and pointed it at a local copy so research never reads the
live SQX install.
catalog.py is now the single place that says what an instrument is: path, asset
class, synthesised spread, and the price range that PINS the decimal scale. The
scale used to be fitted against an MT5 reference series that no longer exists, so
it is now asserted per file instead of inferred, on three checks that agree on 1e6
for every file - price level, round tick GCD, and the medians recorded when the
decoder was last validated at corr 1.000000 (FTSE 7246, WTI 65.4, USDCAD 1.26 all
reproduce exactly). breadth.py's two duplicated dicts are gone; catalog owns it.
Validation: five of the six dual-feed pairs agree at corr >= 0.999986 with a
sub-basis-point median difference. WTI is the exception at corr 0.9988 / -14.7 bp,
because the vendors roll the continuous contract on different days - so the WTI
pair is NOT a clean replication arm and must not be quoted as one.
detect.py answers the question the closed verdicts never did. "No edge" has two
opposite causes that look identical in a results table - the effect was smaller
than the spread, or the window could never have resolved it either way - and only
the second one is fixed by more data. So it computes both bounds per cell: the
win-rate uplift needed to pay the spread, and the uplift that is distinguishable
from chance on the trades the history actually holds.
Also fixed, all found by running the above:
book.py W1 buckets, with the 4-day offset the epoch's Thursday needs, or
every weekly bar would straddle a weekend
breadth.get serves each symbol's finest AVAILABLE base series and refuses to
resample upward rather than inventing intrabar highs and lows
catalog.keys filtered on M1, which silently dropped the futures tree (M60) and
the 33-year SPY series (D1) from every screen that iterated it
breadth.cells one generator, shared, that skips the unbuildable tick-derived arm
instead of dying on it - was duplicated in two test scripts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same doctrine as AltDataFetch's SaveRaw/RebuildFeatures (a8e6572): a plain
FileOpen(FILE_WRITE) truncates on open, and this file carries the daily/total halt
LATCHES - a crash mid-write left a truncated file that LoadState()'s magic/size guard
rejects, silently re-seeding from live equity and clearing a compliance halt. Staged
through a temp file + atomic rename; each FileWrite* call's return value is now
checked so a write failure sets ok=false instead of silently corrupting the state
alongside a successful-looking FileClose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
CFirstPassageLadder (RungFor/StoreBar/FirstTouch/OutcomeR/WinShare),
CTripleBarrier+CLabelOverlap (ApplyMinStopWidening/ComputeLevels/SnapToLadder,
the label-overlap effective-sample-size correction), CMetaFamilies (the
classic-pattern taxonomy + table-naming rule), SGeometryScan::Reset() (guards
against 7452bd1's partial-reset shape recurring) and System/BinomialStats.mqh
(every deploy-gate/edge-floor formula this codebase shares). Each is a small
.mq5 Expert Advisor under Tests\ printing PASS/FAIL per assertion via
Print(), sharing Tests\TestHarness.mqh. All 5 self-compile-verified 0
errors/0 warnings. FirstPassageLadder.mqh/TripleBarrier.mqh expect
BARRIER_LADDER_COUNT/BARRIER_LADDER/BARRIER_HORIZON_LADDER_COUNT predefined
by their includer (normally ExpertSignalAIBase.mqh); the test EAs define
copies matching production values rather than including the whole AIBase
chain. SGeometryScan is reproduced verbatim from ExpertSignalAIBase.mqh for
the same reason, flagged in-file as needing to stay byte-identical.
Also adds Tests\convert_sample_data.py, which runs research/sqxbars.py's
decoder against a COPY of SP500_the5ers_H1.dat (never the SQX install
itself) so the operator has real sample data to point a manual tester run
at. Decodes structurally (52,542 H1 bars, monotonic, 0 high<low violations)
without calibrating a price scale - sqxbars.load()/sqx.calibrate_decimals()
both require a validated reference series to do that safely, which this
self-contained script does not have. Tests\sample_data\ (the raw copy +
decoded .npz) is gitignored, same policy as the existing Market Data/ rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FeatureScreen.mqh's MI/permutation-null diagnostics (mean/best-col
report, excursion report, lag-profile family-wise test, barrier-
geometry scan) and AutoTune.mqh's TuneIndicatorsByFilter install gate
each spelled out the add-one-smoothed Monte-Carlo p-value
(1+atLeast)/(draws+1) independently. Added PermutationPValue(atLeast,
draws) to System/BinomialStats.mqh (returns 1.0 for draws<=0, matching
every existing call site's own guard) and replaced all six inline
expressions with a call to it. Pure arithmetic substitution, no
control-flow change.
ApplyClassificationSoftmax/AdjustedSignalFromSoftmax/DirectionalMargin each
re-derived `pBuy > pSell && pBuy > pNeutral` (and the Sell mirror)
independently, one of them documenting the duplication by comment rather
than eliminating it. Added Argmax3() as the single derivation (ties to
Neutral); all three now branch on its ENUM_SIGNAL result instead of
re-testing the comparison. Pure relocation, statement-by-statement
equivalent - verified by diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>