提交图

938笔提交

作者 SHA1 备注 提交日期
AnimateDread
cb1d86e477 fix(vote): persist the tier ladder - a converged model was mute after every restart
THIS IS NOT A DISPLAY BUG. A deployed model could not vote, or trade, at
any point after a terminal restart, and never would have.

LiveVoteContribution() returns 0 for every call until m_tiersSelfRanked
is set - deliberately, and correctly: before RankTiersFromOos() runs,
m_pattern_0..3 hold the constructor's stock 25/50/75/100, which since the
2026-08-18 currency change is the WRONG UNIT rather than a weak opinion,
and one unranked member would drag the whole ensemble over any threshold.

But that ladder is produced ONLY by a completed pass 3, and it was never
persisted - the code comment at LiveVoteContribution says so outright.
A converged model runs no further passes. So on every restart it lost its
entire vote permanently:

  LiveVoteContribution -> 0  => no live vote          ("0 vote/4 flat")
  ReconstructionWeight -> 0  => overlay divisor 0     ("0 had a snapshot")
                             => no arrows
                             => no fired bars, so g_ensCumOosTotal stays 0
                             => "measuring..." forever

Every symptom reported over the last three exchanges is that one cause.
The log is unambiguous: six H4 charts resumed at era 70/71, all 24
rescans completed with ~2700 Buy / ~2200 Sell per model, and the overlay
then swept 4999 bars finding "0 had a snapshot". The calls were there;
nothing was permitted to count them.

WST7 now stores the four tier weights, the module trust weight and the
self-ranked flag beside the model. Restored only when the stored flag
says the ladder was MEASURED - a .stats written before a model's first
pass 3 holds the stock ladder, and adopting that as if measured is the
exact error the flag exists to prevent.

A .stats predating WST7 has no ladder, so existing converged models stay
silent until their next scoring pass mints one. That case now prints a
warning naming all three of its symptoms, because each one independently
looks like a different bug.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 14:26:33 -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
ee0dc78382 chore(repo): move research/ and references/ out to ..\Warrior_Research
This repo now holds only EA (MQL5) sources. The Python research scripts and the
third-party MQL5/PDF reference material live in a sibling workspace folder,
..\Warrior_Research\, with their own git repo (initial commit ec2214a there).

Nothing in the EA depends on either folder at build or run time, and the research
scripts address ..\Market Data\ and the MetaTrader Common\Files directory by
absolute path, so the relocation breaks no path. EA comments that cite scripts by
name (research/edge.py, research/altdata/export.py, research/test_spread.py, ...)
stay accurate - only the parent folder moved.

.gitignore drops the two rules that only existed for the moved trees
(references/*.pdf, research/edge_rows.npy); they were carried over to the new repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 11:00:24 -04:00
AnimateDread
34180b8c9c fix(exits): never let a declined tick leave an open position unchecked
Refresh() returning false skipped the whole of Processing(), and
Processing() is where CheckClose() and CheckTrailingStop() live. So on
any tick with unusable quote history, a failed RefreshRates(), or a
period-flag mismatch, an already-open position got no exit check at all -
it rode. Invisible by construction: nothing logged, no order sent, and
next tick the position looks exactly as it should. The only trace is a
stop that should have moved and didn't.

ProtectOpenPosition() now runs the CLOSING half of Processing() on those
ticks. Only the closing half, on purpose: CheckReverse() and the
pending-order block both OPEN exposure, and opening on data just declared
unfit to trade on is the opposite of the point. Closing on an imperfect
quote reduces risk even when the quote is wrong; opening on it does not.
When it acts, it says so in the journal - a degraded-path exit should
never be silent.

Also pins the invariant at the Expert_EveryTick gate: that input
throttles how often the EA forms an OPINION, never how often it can act
on a position it already holds. Exits stay above the gate, and the
comment now says so to the next person editing it.

Pre-existing hole, not introduced by the EveryTick work in 6d48fdb -
that change is what made it worth reading the tick path closely.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:52:07 -04:00
AnimateDread
5f7bbd5d6d docs(overview): trade-management enums are placement-only, and pin ordinals
Records the rule the removal exposed: validation catches an enum value that no
longer exists, but not one that silently now means something else. Also drops
the stale claim that SL_Mode/TP_Mode define the training target - the
swing-pivot label is geometry-free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:46 -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
6d48fdb4cd perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
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>
2026-08-25 09:50:27 -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
e035a3076d feat(pool): default cross-instrument training pooling on
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>
2026-08-24 22:38:38 -04:00
AnimateDread
b5d34a82b4 feat(panel): one live vote line, no stale era count, no per-model HUD
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>
2026-08-24 22:27:41 -04:00
AnimateDread
12d9871650 chore(comments): drop two references to the deleted drift verdict
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>
2026-08-24 22:11:50 -04:00
AnimateDread
cdc8b2d1ec fix(enumerations): remove duplicate MARKET entry and maintain trailing strategy consistency 2026-08-24 21:34:22 -04:00
AnimateDread
4ad079aaed fix(topology): size the network against observations, not bars
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>
2026-08-24 21:21:05 -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
95503c2101 fix(target): pivot finality is an event, not a waiting period
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>
2026-08-24 18:40:06 -04:00
AnimateDread
2abca1298c fix(labels): a closed candle shifts the cache, it does not invalidate it
Series indices are relative to now, so one new bar moves every cached bar's
index by one. EnsureBarCachesCapacity answered that by wiping the label cache,
the excursion caches, the ladder and the feature cache and rebuilding the whole
prebuild from scratch - on any timeframe where a bar closes before a run
finishes, the labels were being recomputed continuously and the training set
never held still.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 18:36:16 -04:00
AnimateDread
579e8b45ef feat(target): swing-pivot direction label, and drop the ADZigZag name
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>
2026-08-24 18:26:25 -04:00
AnimateDread
0cd20a5749 diag(geometry): the sweep must not present "least negative" as a recommendation
First 35 eras across both charts, this run:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   Setting DeployOnExpectancy=false restores the previous behaviour exactly.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:15:06 -04:00
AnimateDread
7075747f4a fix(training): a new best must beat the noise; the blank-chart census must name its cause
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>
2026-08-24 15:58:34 -04:00
AnimateDread
c14ffc84e2 fix(training): a yielded pass is not a finished pass - Train() must return
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>
2026-08-24 15:23:20 -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
9c31625aae fix(training-pool): say why a peer was rejected instead of adopting nothing in silence
Two charts (SP500 H4 + USDJPY H4) ran with the pool enabled and produced no
TrainPool directory, no adopted rows and not one journal line. The pool was
inert and there was no way to tell that from "the feature is off".

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:46:58 -04:00
AnimateDread
4c7c063e77 chore(research): drop the scratch placebo driver, now a sqxrepl subcommand
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:31:07 -04:00
AnimateDread
5ac948595b research(metafilter): shuffled-outcome control, and correct the "data adds nothing" verdict
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>
2026-08-24 13:16:40 -04:00
AnimateDread
a86019c46a fix(edge): save_rows walked only the top level, and my first fix was the reason
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>
2026-08-24 13:12:21 -04:00
AnimateDread
1c2030bcb8 research(sqxrepl): measure the hidden long-beta, as a beta subcommand
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>
2026-08-24 13:06:19 -04:00
AnimateDread
5b33c8396b research(sqxrepl): three-arm placebo, and a placebo subcommand to run it
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>
2026-08-24 12:54:44 -04:00
AnimateDread
cc68a768b1 fix(sqxrepl): the "buy and hold" benchmark shared the strategy's own exit bar
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>
2026-08-24 12:42:41 -04:00
AnimateDread
37764981ea fix(sqxrepl): the weekend clock was a day early and keyed on a bar most feeds never trade
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>
2026-08-24 12:35:02 -04:00
AnimateDread
061128a88c research(metafilter): correct the claim that the geometry control carries no information
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>
2026-08-24 12:31:04 -04:00
AnimateDread
22e75741b0 research(metafilter): the geometry control, which turns the finding inside out
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>
2026-08-24 12:27:04 -04:00
AnimateDread
5c3bd493ed research(metafilter): ask whether the data can RANK trades, not call direction
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>
2026-08-24 12:15:28 -04:00
AnimateDread
aa53d5d2ca research(sqxrepl): replay the generated SQX strategy through the validated fill engine
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>
2026-08-24 12:09:52 -04:00
AnimateDread
2c68314526 research: count markets, not feeds - and stop throwing away a 40-minute run's rows
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>
2026-08-24 11:30:54 -04:00
AnimateDread
76dfa223d3 research(edge): one timeframe-scaled table of sample-size floors, and k as an argument
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>
2026-08-24 11:27:27 -04:00
AnimateDread
7106504061 research(edge): matched-fold A/B on training breadth, and three fixes that changed the answer
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>
2026-08-24 11:10:02 -04:00
AnimateDread
349c640c2b chore: ignore the edge screen's regenerated .npy artifact 2026-08-24 10:35:39 -04:00
AnimateDread
4cd6841c07 research(edge): combined price+volume+time+alt screen, built so drift cannot pass as skill
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>
2026-08-24 10:35:31 -04:00
AnimateDread
c5890ef17f research(detect): the ratio was read backwards - rebuild around a decision number
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>
2026-08-24 09:47:55 -04:00
AnimateDread
f67e33a5f7 docs(signals): README described a folder that no longer existed
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>
2026-08-24 09:21:49 -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