提交图

720笔提交

作者 SHA1 备注 提交日期
AnimateDread
afe1038d11 fix(topology): stop a training-alone size becoming permanent, and stop the keep-screen latching underpowered
1. THE POOL FIX WAS LANDING ON A TOPOLOGY THAT COULD NOT SEE IT.

   ComputeFirstLayerWidth budgets against EstimatedInSampleBars, which counts
   this chart's own bars PLUS the training pool. On a COLD fleet start every
   chart derives and pins its topology BEFORE any chart has published a pool
   file - measured on the 18:13 start, model creation at 18:13:21 against a
   first publish at 18:13:48. All six sized as if training alone, wrote that
   into .cfg, and adopted it back on every later start even with the pool full.
   SP500 ran a first layer floored to 16 while adopting 30229 peer rows.

   Adopt-don't-compare exists to protect weights shaped by those sizes. It was
   also running for a model with NO .nnw, where there is nothing to protect and
   the .cfg is just a record of one unlucky moment. The four derived sizes are
   now re-measured when no weights exist.

   Safe on all three counts that matter: free (nothing to discard), cannot loop
   (once weights exist the .cfg is authoritative again), and cannot fragment the
   pool - the derived width is NOT in BuildModelFingerprint, which keys only on
   the FEATURE layout. Verified: field 2 of the fingerprint is
   LEGACY_HISTORY_BARS_SLOT, not the first-layer width.

   TO TAKE EFFECT the weights must be wiped while the TrainPool is KEPT - the
   census has to be non-empty at derivation time. A full wipe empties the pool
   and reproduces the original condition exactly.

2. THE KEEP-SCREEN LATCHED ON AN UNDERPOWERED SAMPLE.

   MI_MIN_SAMPLES is a floor for "can this be computed", and it was being used
   as the bar for "is this answer final". The screen fired on the first era
   clearing 200 rows and latched, measuring at 202-773 samples where a warm
   chart gives ~2065. Columns kept then tracked SAMPLE SIZE rather than
   information - EURUSD kept 0 of 49 at n=202, SP500 kept 15 at n=773, and the
   ordering across all six charts was very nearly monotone in n.

   A thin sample is still measured and printed, but it no longer closes the
   question: below MI_GOOD_SAMPLE_FRACTION of the target the result is labelled
   underpowered and a later era supersedes it, bounded by the same attempt
   budget. An underpowered screen that latches is worse than one that waits,
   because it looks like a result.

Build tag -> fleet-pool-v2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:27:48 -04:00
AnimateDread
a970405042 feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.

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

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

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

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

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

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

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

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

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

Build tag -> fleet-pool-v1.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:53:16 -04:00
AnimateDread
15b028450b fix(vote): follow the derived rung until a checkpoint exists, pin thereafter
A LIVE DEFECT from combining today's two changes. The threshold pins ON
CHECKPOINT (ad4ae58) and the burn-in forbids checkpoints below era 20 (32eb5c5),
so nothing was published for the first 20 eras and those charts sat on the
Signal_ThresholdOpen seed of 25 - an ABSOLUTE WIN RATE under a currency that no
longer uses one. 25 is above what the vote can now reach:

    USDJPY  Filtered view: drew 0 arrow(s). Strongest vote 19.3% vs 25.0% threshold
    SP500   Filtered view: drew 268 arrow(s). Strongest vote 13.1% vs  5.0% threshold

Zero arrows AND zero trades on all three FX charts (eras 10/10/16), while the
three past era 20 published their derived rungs and ran normally.

Fix: publish the current era's derived rung while g_ensBestEra < 0. Before a
checkpoint exists there is nothing to protect, and an arbitrary seed is strictly
worse than the latest measurement. Once a checkpoint exists the pin takes over
unchanged.

HOW IT WAS FOUND: the user said the FX charts were visibly quiet while I was
reporting 17-18% coverage and had declared the quiet-chart problem fixed.
Era-verdict coverage says what the vote WOULD fire on in an OOS replay; it says
NOTHING about whether the live threshold is reachable. The log stated it
verbatim - "Strongest vote 19.3% against a 25.0% threshold" - and I had not
looked at the drawn view before claiming success. Verify a display or trading
claim on the ARROW COUNT, never on the scorer.

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:36:20 -04:00
AnimateDread
326e314b3e diag(features): emit the keep-set as a comparable hex mask
The keep-screen answered whether pruning is worth doing - consistently, across
all six charts:

    chart    kept     width        first-layer budget
    EURUSD   17/52    624 -> 204   11.3 -> 34.3
    USDCAD   19/52    624 -> 228   10.0 -> 27.2
    USDJPY   18/52    624 -> 216   11.2 -> 32.4
    XAUUSD   17/51    612 -> 204    7.8 -> 23.2
    SP500    18/50    600 -> 216    3.8 -> 10.5
    XTIUSD   16/51    612 -> 192    3.8 -> 12.1

~1 column in 3 carries the association and the rate is stable across six
independent charts - noise would not reproduce that tightly. Pruning nearly
triples the capacity budget and lifts XAUUSD off the 16-wide floor. SP500 and
XTIUSD (the two pool-poor charts) improve ~2.8x and still miss it; they need the
12-bar window cut as well, which is a separate lever costing nothing in feature
semantics and not touching pool compatibility.

Headline MI is strong everywhere under the pivot-event label: 0.008-0.0099 nats
against a ~0.002 null, strongest column 0.047-0.077 against a ~0.006 null-max
(8-13x).

WHAT THIS COMMIT ADDS is the last fact needed before a mask can be built: WHICH
columns, as a hex bitmask, so two charts' masks can be compared by eye and by
grep. Identical masks across the fleet mean ONE fleet-wide mask keeps every chart
in a single pool group; divergent masks would split six charts into six groups of
one, and pooling is the only thing currently holding the FX charts above the
capacity floor - so a per-chart prune could cost more capacity than it buys.

Still report-only. No fingerprint change, no retrain forced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:09:47 -04:00
AnimateDread
8c1266db0b diag(mi): name which BuildMiSample exit abandoned the sample
The MI screen collapsed to "-1.00000 nats/feature over 0 permutations" on the
first COLD start after a wipe, taking the new per-column keep-screen with it. On
the same chart seconds earlier the auto-tuner had scored the same function fine:

    auto-tune complete - 12 candidates scored, mutual information 0.00843 nats
    feature/label information - -1.00000 nats/feature ... over 0 permutations

So the data exists and something between the two collapses the sample window.
Cold-start only - every successful report today came from a warm start where the
models loaded from disk, and wiping is what exposed it.

I formed three explanations (label-cache invalidation by the tuner, a shift pad
scaled off an unmeasured label resolution, a zero feature width) and each failed
against the log. Three failed explanations is the point where guessing stops and
instrumenting starts.

BuildMiSample has five distinct -1 exits and the caller can only observe the
collapsed result. Each now names itself and prints the terms that would explain
it: bars, lo/hi, MI_MIN_SAMPLES, OOS split, history window, shift pad and the
measured label resolution the pad scales from. Throttled via TCLog.

Deliberately NOT also "fixing" the latch that makes this stick
(ReportFeatureLabelInformation sets m_miReportDone at ENTRY regardless of
outcome, and the first member then sets g_ensembleChartMiReportDone, so one
failed attempt disables the screen for every member on the chart for the whole
run). If the cause is a genuine cold-start ordering problem, making it retry
would paper over it - the instrumentation decides which fix is correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:57:41 -04:00
AnimateDread
a9e941d7ee feat(features): per-column MI keep-screen (report only)
Step 1 of the prune, stopping deliberately short of pruning - two blockers make
an immediate mask the wrong move, and this is the measurement that decides
whether pruning is worth doing at all.

WHY NOT PRUNE YET:
  * the screen runs with cross-asset ABSENT - its own log line says the numbers
    "describe a NARROWER vector than training will use". A mask built from it
    would have no evidence either way about the cross-asset block.
  * a per-chart mask FRAGMENTS THE POOL. The mask must participate in the
    fingerprint, and the pool only accepts peers with an identical feature
    layout. Pooling is currently the only thing keeping the FX trio off the
    capacity floor - the three pool-poor charts (SP500, XAUUSD, XTIUSD) are
    exactly the three still floored. Six per-chart masks = six pool groups of
    one, and pruning could cost more capacity than it buys.

WHAT THIS ADDS: the per-column MI was always computed inside ScoreMiSample and
thrown away except for the sum and the max. It is retained now, and the same
permutation draws that build the headline null also accumulate a PER-COLUMN null,
which is what a per-column p-value needs - distinct from the null-of-the-max,
which answers the single family-wise question "is the strongest column real".

Selection uses Benjamini-Hochberg at q=0.10, NOT the family-wise bar. FWER
controls the chance of one false positive, which is right for a verdict and far
too conservative for selection - it would discard every genuinely weak-but-useful
feature. BH bounds the expected SHARE of kept columns that are noise, which is
what a feature set cares about.

The report prints the decision in capacity units: columns kept, the resulting
input width, and the first-layer budget before and after against the 16-wide
floor. 3 of 52 is not a feature set; 45 of 52 is not worth a fingerprint re-key.
The cross-asset caveat prints itself when it applies.

Context that makes this worth doing at all: under the pivot-event label the MI
screen now reads "above the noise floor - a real association" - mean 4x the null
(p=0.005), strongest column 7.7x the null-max, excess 0.80% of label entropy,
against 1.3x / 1.15x / ~0.1% under the old label. The noise-floor verdict that
closed several earlier directions was a property of the OLD label.

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

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

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

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

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

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

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

3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).

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

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

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

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

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -04:00
AnimateDread
d059780c22 fix(io): stage atomic writes to a PER-CHART temp, not a shared one
A DATA-INTEGRITY BUG, pre-existing, surfaced by the clearer failure message in
869cd1b putting two identical timestamps next to each other:

    13:42:36.584 (EURUSD) AltRawSave: atomic rename raw_EIA_WPSR.csv.savetmp -> ... failed
    13:42:36.584 (XTIUSD) AltRawSave: atomic rename raw_EIA_WPSR.csv.savetmp -> ... failed

Same file, same millisecond, two charts, a third winning the race. That is not
reader/writer contention - it is THREE WRITERS on one destination, and
AtomicWriteBegin derived the staging name from the destination alone:

    tmpName = finalName + ".savetmp"

So all three opened the SAME temp with FILE_WRITE and wrote it from offset 0 at
once. The published file could be an interleaved mixture of two charts' output,
and the atomic rename publishes that mixture faithfully - the swap guarantees a
reader never sees a HALF-WRITTEN file, and does nothing about a HALF-CORRECT one.
Alt-data is the exposed case: several charts fetch the same series and write the
same Common file.

Keying the temp on symbol+period makes staging private. The rename stays the only
contended operation, and a rename IS atomic, so a loser now publishes nothing
rather than half of itself. It also makes deferred promotion sound for the first
time: the temp promoted later is THIS chart's complete content, never a fragment
of someone else's.

SharedFileCopy.mqh uses the same shape but its destination is agent/terminal-local
and keyed by symbol+fingerprint, so charts cannot collide there. Left alone.

Note the two bugs are independent and both fixes are real. Confirmed in situ at
13:45:45, on the reader/writer one:
    CTrainPoolWriter::Publish: atomic rename TrainPool\USDCAD_16388.bin failed (5004)
    Warrior: deferred promotion of TrainPool\USDCAD_16388.bin succeeded - the peer
    chart that held it has closed it, and the content written earlier is now live
    without rewriting the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:48:43 -04:00
AnimateDread
869cd1b40c fix(pool): defer the atomic promotion to the timer instead of spinning on the tick
REPLACES the in-line retry from ad4ae58, which was the wrong shape and did not
work. Measured after deploying it:

    atomic rename ... failed (error 5004) after 4 attempts

MQL5 exposes no FILE_SHARE_DELETE, so a rename CANNOT succeed while any reader
holds the destination open - it is not a lock that waiting longer wins. The
retry assumed a peer holds a pool file for "tens of ms"; USDCAD_16388.bin is
134 MB and a peer reading it holds the handle for SECONDS. The loop lost every
time and bought nothing but 75ms of tick latency on the failure path.

The content is already written and correct - only the SWAP is blocked. So try the
rename once, and on failure remember the temp and promote it from OnTimer, where
I/O belongs. Once the reader closes, a single FileMove lands it. That beats the
old fallback of waiting for the next full publish, which rewrites all 134 MB and
may be an era away.

  * pending list is bounded (8) and deduplicated - AtomicWriteBegin reuses one
    temp name per file, so a second failure for the same file must not take a
    second slot. A full list falls back to the previous next-publish behaviour.
  * a successful write FORGETS any queued promotion for that name, so a stale
    temp can never overwrite fresher content.
  * a vanished temp (a later publish succeeded outright) is dropped, not retried.
  * a landed promotion is LOGGED. Silence is what made me misread the last
    attempt as working when there had simply been no contention in the window.

Compiled clean; NOT yet run - and note that verification needs a collision to
occur, which happened ~27 times across a whole day. Absence of the message in any
one window is not evidence either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:41:35 -04:00
AnimateDread
72dba892cc fix(chart): configure the vote-arrow layer with the PINNED threshold
Second instance of the same regression b1c3a89 fixed in the .stats loader, found
by reading the log after it: the arrow store was still configured with the
Signal_ThresholdOpen SEED while the chart traded a derived, pinned rung.

    SP500: queued 366 combined-vote arrows for progressive restore
           (threshold to open 25%)     <- chart was trading 15%

CVoteArrowStore carries the identical compare-and-discard guard - "an arrow is a
claim about a threshold, so a changed threshold makes every stored arrow a claim
about a strategy that is no longer configured". Handing it the seed therefore did
both harms at once: discarded restorable arrows whose stored threshold was the
real one, and labelled whatever survived with a threshold the EA does not use.

Root cause is the same in both places: c6eb908 changed the threshold from an
input into a derived value, and two separate consumers still assumed the input.
Worth remembering as the shape of this bug rather than the instances - anything
that PERSISTED the old threshold had to be re-checked, not just anything that
read it.

Reads g_ensDerivedThreshold, which LoadModelStats() has already restored by this
point in init (its "restored the ensemble record" line prints before the arrow
queue line, which is what makes reading it here safe), and falls back to the seed
before the first era has ever been scored. Display-only: the trade path already
had the right number from PublishVoteThreshold().

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 12:09:36 -04:00
AnimateDread
b1c3a898aa fix(persist): adopt the pinned threshold on load; trim the accuracy label
THE REGRESSION, mine, from c6eb908. LoadModelStats() dropped the whole ensemble
record unless the stored threshold EQUALLED the live one. That was right while
the threshold was an operator input - a record built at 25% says nothing about a
chart now running 15%. Once the threshold became derived and pinned the
comparison inverted its own meaning: at load time g_ensembleVoteThreshold is
still the Signal_ThresholdOpen SEED, so the stored derived value never matches
and the record is ALWAYS dropped. Two things died with it, silently:

  * g_ensDeployApproved - a DEPLOYED ensemble came back as a training one on
    every restart, discarding the family-wise deploy it had earned.
  * the pinned threshold itself - PublishVoteThreshold() only fires on a positive
    g_ensDerivedThreshold, so a deployed chart would have traded the .chr seed
    instead of the rung its deploy was certified at. certified != traded, the
    defect 2c443ba fixed, reintroduced three commits later.

Not yet observed live only because SP500 deployed at 10:20, after the last
restart at 09:54, so no restart has crossed a deployed state.

Now ADOPTED, not compared: threshold, counts and deploy flag restore together,
the only coherent state - the counts were conditional on that threshold, which is
why it is stored beside them. Same doctrine as the .cfg topology: adopt what the
model was certified with, never re-derive it underneath a checkpoint. The
most-complete-copy guard is unchanged. It now logs what it restored.

THE PANEL LABEL. "Vote win rate: 34% (338 calls at or above the 15% threshold,
this era 31%)" -> "Accuracy: 34%". The call count, threshold and this-era figure
are diagnostics, all present in the era log line, and on a panel they buried the
one number anyone reads. The threshold no longer needs naming either: it is
derived and pinned rather than an operator's choice, so it is not a caveat on the
percentage. The era/models/deployable suffix appended at era end goes with them.

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 11:59:25 -04:00
AnimateDread
ad4ae58814 feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.

It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.

Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.

THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.

THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.

Retrain-neutral. Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
AnimateDread
34f1e09372 feat(magic): assign the magic number once, then remember it
Expert_MagicNumber = 0 (the new default) means "draw one and write it down".
On first attach the EA picks a random magic in a distinctive band, persists it
to MQL5\Files\Warrior_<symbol>_<period>.magic, and reads that same value back on
every later start. Unique without anyone typing it, and STABLE.

Stability is the whole point. The magic is how the EA recognises its own
positions - a fresh one per start would leave every open position invisible to
the scheduled close-all, the risk-budget flatten and the journal's MAE/MFE walk:
trades still running that no code would ever manage again. So the value is
persisted before it is ever used to trade.

Stored TERMINAL-LOCAL rather than in Common\Files\Warrior_EA, on purpose: that
folder is the one wiped for a retrain, and positions outlive retrains. It also
gives two terminals on the same symbol different magics, which a chart-identity
hash could not.

Fallbacks, both of which stay stable without a file:
  * tester/optimizer/forward use a magic derived from chart identity, so two
    identical passes cannot differ.
  * an unwritable file falls back to that same derived value, and says so.

Books occupy EVEN slots only, so one chart's short book (base+1) can never land
on another chart's long book.

WarriorOwnsMagic() now also recognises the legacy 2024/2025 pair permanently.
Without it, switching an existing chart to 0 while a position was open would
orphan that position. Every caller also matches the symbol, so claiming those
values can only reach positions on this EA's own chart.

Existing charts are untouched: MT5 stores inputs per chart, so the six live
charts keep the 2024 they already have and keep managing what they hold.

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:35:58 -04:00
AnimateDread
17270ab308 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.

WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).

Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.

The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.

Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.

Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.

Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00
AnimateDread
c6eb9085d5 feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.

Measured on 619 era verdicts across all six live charts:

  * every era on every symbol had at least one rung clearing the full gate.
    At the fixed 25% the fleet was actually running, four of six symbols had
    none, ever. The threshold, not the models, was the blocker.
  * walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
    31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
    Near-zero shrinkage - a measurement, not a fit. It holds because the
    binding constraint is COVERAGE, a near-deterministic step function of the
    vote distribution, not precision.
  * vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
    vs a fixed 20%: deployable on all six rather than four of six.

Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.

Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.

Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
AnimateDread
51620fe9e0 fix(vote): Signal_ThresholdOpen 25 -> 15, from the sweep's own numbers
The quorum model that said 20 was wrong. It reasoned from the vote's
quantisation - 4 members at tier weight ~30, so 3-of-4 agreeing gives
22.5 and PCT_20 admits it - and simultaneous agreement turns out to be
rarer than a per-member coverage of ~27% implies. Measured on real rows
by the threshold sweep added in the previous commit:

  symbol  floor    15% cov/prec    20% cov/prec    25% cov/prec (active)
  EURUSD   6.7%    15.7 / 32.6     12.5 / 33.7      4.1 / 35.0
  SP500    6.9%     9.8 / 32.7      4.0 / 34.4 X    1.0 / 34.7 X
  USDCAD   7.2%    18.9 / 32.2     12.8 / 32.6      4.0 / 34.7 X
  XAUUSD   6.7%    12.8 / 29.2      4.4 / 33.9 X    1.0 / 26.5 X
  XTIUSD   6.9%    11.4 / 34.3      7.8 / 35.7      1.7 / 38.4 X

15 clears the coverage floor on every symbol; 20 fails SP500 and XAUUSD;
25 fails all of them. The precision surrendered is about 2pp, because
precision is nearly flat across these rungs while coverage moves 10-20x -
the high thresholds were buying almost nothing for the coverage they
cost. The rule this encodes is: take the cheapest rung whose coverage
clears the floor, not the best precision. Precision above the bar earns
nothing extra; coverage below the floor makes the era undeployable
regardless.

Not in BuildModelFingerprint(), so every trained .nnw survives.

DOES NOT MOVE THE RUNNING FLEET. MT5 stores input values per chart in
profiles\Charts\*\chart*.chr, so this only takes effect on a fresh
attach; the live charts have to be changed in each one's EA properties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 04:24:37 -04:00
AnimateDread
b9da557e4e diag(gate): report what the vote would score at every threshold rung
The gate could say "coverage too low" but never "and here is what it
would be one rung down", so the single parameter most responsible for a
refusal was the one its own output said least about. Working it out by
hand needed a model of the vote's quantisation (a weighted mean of member
tier weights, so the threshold is really a quorum) and that model could
not be checked: MT5 stores the input PER CHART in profiles\Charts\*
\chart*.chr, so an already-attached EA ignores a changed source default -
confirmed by a full close/recompile/relaunch after which the log still
read "fired at vote>=25%". There was no cheap A/B available.

Each era now reports coverage and precision at every PERCENTAGE_PRESETS
rung from 5% to 30%, measured on the same rows the verdict just scored,
marking the active rung and any rung that clears the coverage floor. It
is accumulated before the live threshold test so the sweep sees every
scored row, and gated by the same direction policy so its numbers are
comparable with what the gate certifies. Nothing reads it to decide
anything.

Motivation, measured overnight across 534 eras with zero runtime errors:
every symbol clears its precision bar and every symbol fails on coverage
(0.0-3.3% against a ~6.7-7.2% floor), while the members stay healthy
throughout at 22-27% precision against a 13-14% chance rate on 25-38% of
bars. Only the aggregation fails. SP500 was DEPLOYABLE at era 5 with 7.5%
coverage and sits at 0.7% by era 536 with precision unchanged - more
training is proven not to help, because a 25% threshold against ~30 tier
weights demands unanimity and the models diverge as they specialise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 04:17:27 -04:00
AnimateDread
1533365a85 diag(gate): the coverage refusal contradicted itself
The message I added one commit ago printed, verbatim:

  "Precision was 28.4% against a 39.1% bar, so the calls it DID make
   were NOT good enough: the vote is too selective, not too weak."

Those two clauses say opposite things. Only the "NOT" was conditional;
the diagnosis after the colon was hardcoded, so whenever precision missed
its bar the line asserted and denied the same thing in one sentence.

The two cases are opposite diagnoses and must not share a sentence:

- Precision CLEARED its bar -> the calls were good and there were too few
  of them. The vote is too selective.
- Precision MISSED its bar -> this is still not "the model is weak",
  because the exact-binomial floor is computed from the INDEPENDENT call
  count, so thin coverage inflates the very bar it is judged against.
  Reporting that as a second, separate failure sends a reader off to fix
  the model when coverage is what moved the target.

Caught by reading the diagnostic's own first live firing rather than by
review - the same way the two regressions before it were found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:47:03 -04:00
AnimateDread
0f756faf2a diag(gate): name the deployability condition that actually failed
The stage-3 refusal read "no era's combined vote ever cleared the
deployability floor" and then listed all three conditions in one
parenthesis - fires on a quarter of the base rate, both directions alive,
precision above the reference by 2 sigma - without saying which one fired.
The three have nothing in common as fixes, so the list was not a
diagnosis. It cost real time to work out by hand tonight, and the answer
was coverage every time.

Keeps the best era's coverage, its floor and its precision bar alongside
the win rate already retained, and names the failing condition. The
coverage branch also states whether the calls it DID make cleared the
precision bar, because "too selective" and "too weak" are opposite
problems that the old message could not distinguish, and points at
Signal_ThresholdOpen being a quorum rather than at the models.

Cleared at both existing reset sites so a refusal can never describe an
era that is no longer the best.

Context: SP500 reached stage 3 at era 67 and was refused on coverage
0.5% against a 6.9% floor while its precision was 62.5% against a 61.4%
bar - i.e. the vote was too selective, not too weak. Same doctrine as
CTrainPoolReader::Announce's reject list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:14:51 -04:00
AnimateDread
2e22e714c6 fix(pool): length-prefix the fingerprint - the cross-instrument pool was inert
STrainPoolHeader wrote its fingerprint into a FILE_BIN stream as
FileWriteString(h, fingerprint + "\n") and read it back with
FileReadString(h) - no length argument. In binary mode FileWriteString
emits the characters raw: no length prefix, no terminator, and "\n" is
just another character rather than a delimiter anything honours. The
reader had nothing to stop at, over-read into the float rows that follow,
and returned the fingerprint plus a few bytes of binary garbage - so
`fingerprint != wantFp` could never succeed between two genuinely
identical models.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:41:41 -04:00
AnimateDread
994fe3899c feat(label): pivot-EVENT target replaces direction-to-next-pivot
The old target asked "which way is the next pivot", which every bar of a
~13-20 bar leg answers identically - so the net could not tell a fresh turn
from mid-trend and learned the prevailing direction instead. Its own
zero-skill reference showed it: chance sat at 56/44, i.e. the label WAS the
drift, and the gate's standing warning ("a model that only reproduces it has
found the drift, not an edge") applied to the target itself.

Buy now means a swing LOW commits within PIVOT_LABEL_TOLERANCE_BARS bars,
Sell a swing HIGH, Neutral no turn that close. Pivot type is read from
ZigZagBuffer[p] == Low[p], exact by construction in ZigZag.mq5. The existing
P1-final-once-P2-commits rule is kept and now also settles the NEGATIVE
verdict, so the Neutral majority is permanent rather than provisional.

Measured on a full fresh run, all 6 charts:
  class balance   56/44/~0     -> 13.7/13.7/72.6 (imbalance 5.3:1)
  label overlap   ~31 bars     -> 5 bars
  independent obs 368-1086     -> 2331-7032
  weights/obs     9.2-26.2     -> 1.1-4.2
  coverage        100% of bars -> 17-48%
  23 of 24 models fire all three classes at precision 18-32% vs 13-15%
  chance; SP500's ensemble reaches DEPLOYABLE (32.3% vs a 24.0% bar).

Two bindings had to move with the label:

- The capacity deflator. m_swingLifespan fed EstimatedInSampleBars() as
  raw/31, measured from the legs. Overlap is now a property of the LABEL -
  one turn is callable by exactly the tolerance window - so it is the
  window, not a leg measurement. Missing this would have kept every model
  sized for a sixth of its real evidence.

- A dormant cold-start seed. Labels.mqh seeds the output bias toward the
  dominant class above COLD_START_SEED_MIN_DOMINANCE (0.70); at 56/44 it
  never armed, at 72.6% Neutral it does - writing a fixed +-3.0 against a
  true prior spread of ~1.75, which would start every net predicting Neutral
  ~95% of the time. Now seeds the measured log-prior, zero-centred and
  capped by the same guard rail the logit adjustment uses (Lin et al. 2017).

TGT:SWG1 -> TGT:PVT1:<tolerance>, with the window in the token because it is
part of the label: every .nnw is invalidated and the fleet retrains.

Depth is still gated, and now for a precise reason: the first dense layer
stays at FIRST_LAYER_MIN_WIDTH because budget = effN/(inputWidth+1) is 11.2
at input 624. Reaching the next rung needs inputWidth <= ~218, i.e. feature
pruning - not architecture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:16:16 -04:00
AnimateDread
6adb710a79 fix(binomial): correct tail calculation in BinomialUpperTailP and add tests for accuracy 2026-08-25 23:37:22 -04:00
AnimateDread
0fddaeea12 fix: correct edge floor percentage calculation and logging for model training 2026-08-25 23:16:05 -04:00
AnimateDread
b2784b5a4d Enhance Feature and Topology Interfaces with Bulk Operations and Cache Management
- Added bulk read/write methods for feature caches in IFeaturesView and its implementations to optimize performance.
- Introduced LabelCacheInvalidateAll method to manage label cache invalidation alongside feature cache.
- Implemented PooledIndependentBars method in topology interfaces to account for additional independent observations.
- Enhanced risk budget management with throttling for peak-equity updates to reduce unnecessary file operations.
- Improved error handling and logging for ATR trailing stops to ensure better visibility of issues.
- Updated alt-data handling to prevent unnecessary operations during testing and optimization phases.
2026-08-25 22:51:50 -04:00
AnimateDread
ccfe5563e3 perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.

Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.

OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.

batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
AnimateDread
bd46374954 perf(train): DLL-side mini-batch apply + 300ms slice - the era bottleneck
"Hundreds of times slower than a regular EA" decomposed into two
multiplied factors, both measured:

1. THE OPTIMIZER STEP RAN IN INTERPRETED MQL5. The CPU tier shipped
   the F4 accumulate exports with deliberately no matching apply
   (WarriorCPU.h said so), so on the DLL backend - this box - every
   TRAIN_BATCH_SIZE=8 batch fell to the host loop in ApplyAccumToBlock:
   a per-weight MQL5 pass through CBufferDouble.At()/Update() plus four
   full weight-matrix BufferRead/Write round trips. The 2026-07-26
   profile had already shown the per-sample Adam step at 81% of ALL
   runtime (feedForward: 8%; feature building: 0.35%) - sqrt+divide
   per weight vs one multiply-add; moving it into MQL5 made it worse.

   New CPU_ApplyAccumAdam / CPU_ApplyAccumMomentum: one element-wise
   ParallelFor takes the batch-mean step and zeroes the accumulator
   DLL-side, generic over any flat block (dense/conv/LSTM/batch-norm -
   all apply paths funnel through ApplyAccumToBlock, which now tries
   the DLL first, with the same one-warning failure latch as the
   OpenCL fast path). Math is the shipped step to the last clamp:
   sqrt-stored v, ClampDelta, AdamW decay, ClampWeight.

   batch_accum_check extended (check 6) and ALL PASS: apply == host
   reference at B=8/B=4, accumulator zeroed, and B=1 accumulate+apply
   == the unbatched Adam kernel BIT-EXACTLY (kernel-vs-kernel, no
   transcription). DLL rebuilt with the shipped /fp:fast recipe.

2. A 24% DUTY CYCLE. Train sliced 120ms per 500ms timer period
   (30ms/member x4), leaving the chart thread idle 76% of the time.
   Now 300ms total (75ms/member): ~60% duty, ~2.5x, click latency
   bounded at ~300ms while training runs - between the fully-reactive
   120 and the documented "sticky drag" 480.

DEPLOYMENT COUPLING: the new .ex5 #imports the new exports, so it will
NOT LOAD against the old WarriorCPU.dll ("cannot find function"). Copy
DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed) in the
same step as deploying the new .ex5.

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

Three changes:

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
AnimateDread
b13a68e411 chore(panel): trim the live vote line to direction, magnitude, voters, verdict
Drops "peak N%", "need N%" and the "armed (bar still open)" middle
verdict state - three pieces that were useful while tuning
Signal_ThresholdOpen but add nothing once a chart is settled and running.
m_votePeak is still tracked (nothing programmatic reads it via this
line), just no longer printed.

The verdict collapses back to two states: "training, not tradable yet"
(undeployed) or TRADE/no trade (deployed) - fires is already forced
false on a prospective vote, so "no trade" falls out for a bar that
hasn't closed without a separate word for it.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:01:24 -04:00
AnimateDread
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