forked from animatedread/Warrior_EA
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
844aac653a |
fix(train): the OOS final pass ran a full epoch at an undecayed rate
Capping the pass at m_etaCeiling was not enough. Measured on the first two live runs: USDCAD 0.00085 over 13,335 bars, EURUSD 0.00242 over 15,041 - a 3x spread across charts, because a chart whose plateau ladder reset recently still carries a high eta and the cap never bound. The slice turns out to be roughly HALF the data, not a tail, so one pass over it at the model's own rate is a full training epoch on a model that has already been selected and certified. That is materially more than the 'just a bit finer weights' this was asked for. OOS_FINAL_PASS_ETA_SCALE (0.25) now scales the rate. Scaling rather than shortening the pass keeps the whole slice in play - seeing the held-out bars at all is the point - while making the step proportionate to an already-selected model. USDCAD and EURUSD have already taken the unscaled pass; that is not reversible without a retrain. USDJPY has not converged yet and will get the corrected one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3218db4a38 |
feat(train): ONE pass over the held-out slice at deploy, on the restored checkpoint
The OOS slice is the newest history and the model never trains on it, while online learning adapts to every bar resolving AFTER deployment. That leaves a gap exactly at the handover, over the most regime-relevant data there is. This closes it: select on validation, then refit on everything, which is standard practice. Placed AFTER Net.RestoreWeights() and ResetOptimizerState() and BEFORE PersistDeployedModel(), so it refines the weights that were actually SELECTED rather than whatever the run happened to end on, and what it produces is what gets written down. THE COST IS REAL AND IS NOW STATED IN THE LOG. The deploy line promises "every model reverts to the weights it held at the era whose combined vote scored best, so the ensemble that trades is exactly the one that was measured". After this pass that is no longer literally true, so the pass prints that the certified numbers belong to the PRE-PASS weights and must be quoted that way. Set EnableOosFinalPass=false to keep certified == traded exactly. Guards: * ONE-SHOT PER RUN, and the flag is set BEFORE the loop so no early return inside it can leave the pass eligible to fire twice over bars it already trained on. Reset at m_trainRunActive=true, because a retrain is a fresh selection and earns a fresh pass. * THE CONVERGED RATE, never a plateau-boosted one: m_modelEta can still carry PLATEAU_RESTART_BOOST from an escape attempt, and this is a refinement of a selected model, not another warm restart. g_eta is what backProp reads, so that is what is capped and restored. * OLDEST -> NEWEST. Series indices count backwards, so decreasing i moves forward in time - the order the bars happened in. * A failed feedForward is never followed by backProp; the output layer would still hold the previous sample's activations and the update would be this bar's label against another bar's prediction. * m_oosFinalPassCutoff records the newest bar consumed and is deliberately NOT cleared on a new run, so a later run can say plainly that its out-of-sample window reaches back into bars this model has already seen. Expect the gain to come from CURRENCY rather than finer weights: OOS precision was measured flat from era 20 while in-sample error kept falling, so the data this model can already see is exhausted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6308a19f27 |
feat(signal): make the signal cooldown tunable, and add a hard any-direction gate
The declustering the charts needed already existed - NmsLiveAccept, per-direction run-collapse plus cross-direction resolution plus strict alternation - and it was already set to 10 bars. It could not be TUNED: SignalClusterWindow was a compile- time const, so finding the right value needed a rebuild. That is the actual gap. Now three inputs, as enum dropdowns: Signal_CooldownScope per-direction, or a hard any-direction gate on top Signal_CooldownBars SCB_OFF..SCB_50, default 10 Signal_CooldownMinutes SCM_OFF..SCM_1440, overrides bars when set Minutes resolve against the CHART period and round UP, so a cooldown asked for in wall-clock is never silently shorter than requested and survives a timeframe change. SCB_/SCM_ prefixes are deliberately unique. M15/M30/M60 are ALREADY members of NF_LOOKBACK_PRESETS, and MQL5 binds a duplicated enum member to the first-declared enum silently - the obvious names would have compiled straight into the news filter's values. THE ANY-DIRECTION GATE IS ADDITIVE, NOT A REPLACEMENT, and the first cut of this had it backwards. Measured on the live log: the current rules draw 222 arrows over 4999 bars, while a BARE 10-bar cooldown permits up to 454 - because ALTERNATION is what declutters today, not the window. Swapping the rules out would have roughly doubled the clutter it was asked to remove. Layered, it can only ever suppress more. Suppressed bars still advance the per-direction last-SEEN cursors, so a run straddling the boundary does not restart as if it were fresh. Applied at all THREE sites that must agree - live inference, OOS pass-3 scoring and the chart renderer. Their own comments say why: an arrow set that does not obey the same rule as the traded set shows calls the EA would never take. Also corrects a stale comment that called this window "display only". It is not: when it suppresses, the live path zeroes the signal outright - no arrow, no vote, no position. Training never sees it, so these cost no retrain and are correctly absent from the fingerprint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1a9b56e3b0 |
diag(label): expose bars-to-pivot - the confound the payoff test was missing
CORRECTION to what the payoff instrument was measuring. The 5-bar horizon looked like the powered test and it is confounded. SwingPivotDirectionLabel returns Buy when a swing LOW lands up to PIVOT_LABEL_TOLERANCE_BARS bars AHEAD, and says the quiet part itself: gating on where the pivot sits relative to entry "would drop exactly the bars where the turn has not finished coming to us", and how much adverse move remains before the turn "is a trade-management question". So on a CORRECT Buy call price is often still falling for d more bars. A window shorter than d measures the APPROACH, not the leg, and its negative contribution is expected on the calls that are RIGHT. The tight null at 5 bars (-0.012 +/- 0.074) is therefore not evidence of no payoff. Neither horizon is both clean and powered: 5 bars is powered and confounded, 18-19 is clean and has an SE of 0.277. (idx - P1) was computed in the label and thrown away. Now cached beside m_labelResolveAge under the same validity flag, and bucketed in the era verdict. DELIBERATELY NOT USED AS A PER-CALL HORIZON, which is the trap sitting right next to this: d exists only on bars the label found a pivot for, so a horizon that varied with d would hand correct and incorrect calls different windows and bias the comparison outright. The horizon stays fixed; d only buckets. The bucket for "the label called no pivot here" is reported by name rather than folded in, because it is the control the others are read against. Buckets 1..N condition on the label, so they describe the MECHANISM, not what a book earns. Reads: rising with d means the edge is in EARLY calls and the tolerance window is spending it - fixable by reweighting the loss, not by a new label. Flat means that hypothesis dies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
98f485b901 |
fix(gate): the payoff horizon ended before the pivot it was measuring
The first cut measured payoff over SwingLifespanEstimate() bars. That is
PIVOT_LABEL_TOLERANCE_BARS - a constant of the TARGET describing how many bars
share one pivot event - and it is the wrong horizon for what a call is worth.
The label fires when a pivot lands WITHIN that window. So at that horizon the
pivot may only just have committed, and a perfectly correct call can still show
a negative forward move because the turn it predicted has not had one bar to
run. Measuring only there would understate the payoff of a signal working
exactly as designed, and could inflip its sign.
Measures two horizons and reports both:
SHORT = PIVOT_LABEL_TOLERANCE_BARS "has the pivot arrived" - a control
HOLD = that + the median ZigZag leg the pivot PLUS the leg it opens,
which is how long a trade on this
call would actually be held
Adds CTopology::SwingLegMedianBars(). It is deliberately NOT the same thing as
SwingLifespanEstimate() and the declaration says so: the lifespan is a constant
of the target and is what the effective-sample-size deflation divides by, while
the leg median is a measurement of the chart and is how long the move runs.
Conflating them is what produced the wrong horizon in the first place.
Non-const and lazily measured, because a model that adopted its .cfg never
walked the chart and would otherwise report HISTORY_BARS_FALLBACK as if it were
a measurement - the same lazy pattern DeriveHistoryBars() already uses.
Reporting both horizons is also the guard against picking one and calling it
the truth. A break-even conclusion here has already been overturned once purely
by getting a horizon wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
43c1b27654 |
feat(gate): measure what a call was WORTH, not only how often it was right
The ensemble deploy gate certifies PRECISION against a chance rate and has never known whether a correct call pays for its own spread. Every verdict this project has recorded - 33% precision against a 14% chance rate, an edge that clears its exact-binomial bar comfortably - is silent on the one question that decides whether any of it is tradeable, and the cost boundary is exactly where several earlier edges died with their precision already believed. Adds a per-row payoff measurement, taken once per ROW (a chart property, not a member one) at the same time the label is written: * forward close move over K = round(SwingLifespanEstimate()) bars, * the up and down extreme excursions over the same window, each divided by the bar's own ATR. K is deliberately the label lifespan the effective-sample-size deflation already uses, so precision and payoff describe the same window and can be read in one sentence. POLICY-FREE: no stop, no target, no trailing rule. It measures the SIGNAL, not a trade-management choice layered on top - exit shaping moves payoff around without creating any, so mixing the two would hide which was responsible. Stored unsigned by direction; the sign comes from the vote at verdict time, and a short's excursions SWAP rather than negate - negating them would report a short's worst case as a negative best case. The newest K bars of the OOS slice have no forward window and are dropped from the tally with their own denominator, never counted as a zero move: that is the leading-edge trap that made the lag profile's first run a false positive. The era verdict now prints mean R, MFE and MAE at the certified rung against the spread in the same ATR units. It GATES NOTHING - wiring a policy to an unvalidated payoff number is how a measurement becomes a decision before anyone has checked it. Build tag payoff-v1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0e1e952b96 |
feat(topology): cap the input window at 6 bars for capacity - 588 inputs -> 294
Three charts (SP500, XAUUSD, XTIUSD) sat on the FIRST_LAYER_MIN_WIDTH floor
even after pooling took SP500 from 4.1 to 1.8 weights per independent
observation. ComputeFirstLayerWidth needs width <= ~331 to clear it; 49 columns
x 12 bars = 588.
TWO QUESTIONS, AND THE WINDOW IS NOW THE SMALLER ANSWER. The ZigZag ladder
answers "how far back is a swing worth looking" and says 12. The capacity
budget answers "how far back can this much data support" and says 6. Taking the
min stops the first writing a cheque the second cannot cover.
WHY THE LAG AXIS AND NOT THE COLUMN AXIS - the choice was between this and a
per-column mask (designed, parked on feature/column-mask):
- On the LAG axis there is a measured null. The corrected lag profile finds no
linear structure at any lag within +/-50, on all six charts, family-wise
p=1.0000, argmax scattered across different columns and lags per chart.
- On the COLUMN axis the two measures that would justify a mask - marginal MI
retention and variance share - are explicitly blind to joint and temporal
structure, and the columns they would delete include the entire price core,
which is the one place such structure would plausibly live.
Cutting where there is a measured null beats cutting where the instrument
cannot see. Corroborating: PAI/CONV/LSTM/HYBRID score within ~1pp of each
other, so the temporal machinery is not visibly earning the deeper lags.
THE CAP IS A FLEET CONSTANT, NOT A PER-CHART DERIVATION. Pool rows are keyed on
`bars x columns`, so a capacity cap computed from a chart's own observation
count would differ across the fleet by construction and hand every chart its
own layout, its own fingerprint and its own pool of one - exactly what orphaned
SP500. Set from the most starved chart; every chart shares it.
Conv survives: CONV_RECEPTIVE_FIELD_BARS is 3, so a 6-bar window still leaves 4
sliding positions. LSTM sequence length becomes 6.
RETRAIN-FORCING and POOL-INVALIDATING: width changes, so old .nnw and old
TrainPool rows are both incompatible. Wipe both - which puts the fleet back in
the cold-start condition
|
||
|
|
1e6d00e602 |
fix(vote): a resumed converged model passed the eligibility test and then abstained on every bar
Found by restarting the terminal against three charts that had just deployed - the exact scenario |
||
|
|
6c2959dc32 |
feat(topology): re-derive capacity once when the training pool appears
A cold fleet start sizes every model BEFORE any chart has published a pool
file, so the first layer is budgeted as if the chart trains alone and then
pinned to .cfg. This is not a rare race - it is what happens EVERY time the
feature layout changes, because that invalidates the pool and forces a wipe.
Correcting it by hand needs a two-phase start: run the fleet to fill the pool,
stop, wipe the weights while KEEPING the pool, restart so derivation sees it.
That is not something an unattended fleet can do for itself, and getting it
wrong is silent - the models simply stay narrow.
TuneIndicatorsAndTrain now notices that the pool has appeared and re-derives
once, reusing ResetWeights() - the existing tested path that re-measures all
four sizes, rebuilds and rewrites the .cfg. No second copy of that logic.
Bounded on every axis that could make it a loop:
- once per model (the flag is set BEFORE the reset, because ResetWeights
zeroes m_eraCount and the model would otherwise re-qualify forever)
- only while era <= CAPACITY_RESIZE_MAX_ERA, so the discarded eras are worth
nothing
- only on CAPACITY_RESIZE_MIN_GROWTH real growth
- only if the recomputed width actually differs; if it does not, the check
settles itself rather than re-running the census every era
Safe against the one thing that would make it self-defeating: the derived width
is NOT part of BuildModelFingerprint, so a model that resizes does not leave
the pool it resized for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
00699e1af8 |
fix(chart): stale combined-vote arrows survived every wipe, because two files lived outside Warrior_EA\
Operator report: arrows labelled as restored from a previous session on a fleet training from era 0. Confirmed - all six charts restored 115-431 combined-vote arrows drawn by models that no longer exist. TWO INDEPENDENT DEFECTS, either of which alone causes it. 1. CVoteArrowStore::Discard() HAD NO CALLER. The member-scoped .arrows file is cleared by ClearPersistedChartSignals on a fresh topology. The CHART-scoped .votearrows store has an equivalent Discard(), written for exactly this, and nothing ever called it. The store is keyed on the DB config fingerprint, which does not move when a model is wiped, so it reloaded across any reset - fresh topology, panel weight reset, or a model-file wipe. A vote is a claim made by a specific set of members. If any member rebuilt from scratch this run, the whole stored history is void, so g_warriorFreshTopologyThisRun is now raised wherever a member discards weights or builds a fresh topology, and the store Discards instead of Loads. 2. TWO WARRIOR FILES LIVED OUTSIDE Warrior_EA\. .sigvis and .votearrows were written to the ROOT of Common\Files, outside the one directory that "wipe the Warrior EA files" has always meant. Two consecutive wipes this session left them standing untouched, and neither wipe was as fresh as reported. Both now live under Warrior_EA\ChartState\. A wipe that does not remove all of a program's state is not a wipe, and nothing in the log told the operator which files were missed. NOTE for anyone re-running the wipe: pre-existing WarriorVote_*.votearrows and Warrior_EA_*.sigvis in the Common\Files ROOT are orphaned by this change and should be deleted once. Build tag -> fleet-pool-v3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
b1c3a898aa |
fix(persist): adopt the pinned threshold on load; trim the accuracy label
THE REGRESSION, mine, from |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
0fddaeea12 | fix: correct edge floor percentage calculation and logging for model training | ||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
b92e233b88 |
fix(chart): a deployed model rescans history to rebuild its vote arrows
The sidecar added in
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
2abca1298c |
fix(labels): a closed candle shifts the cache, it does not invalidate it
Series indices are relative to now, so one new bar moves every cached bar's index by one. EnsureBarCachesCapacity answered that by wiping the label cache, the excursion caches, the ladder and the feature cache and rebuilding the whole prebuild from scratch - on any timeframe where a bar closes before a run finishes, the labels were being recomputed continuously and the training set never held still. The labels do not change when a candle closes. ShiftBarCaches moves every per-bar cache up by the number of new bars, marks only those newest bars as unfilled, and leaves the rest exactly as computed. CFirstPassageLadder gets a matching Shift (resizing directly rather than through Allocate, which zeroes the ages this is preserving). Refuses, falling back to the full rebuild, when a prebuild is mid-flight: its cursor is an index into the array being moved. Compile-verified in the staging copy: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
579e8b45ef |
feat(target): swing-pivot direction label, and drop the ADZigZag name
TARGET_SWING: the direction models learn which way the next CONFIRMED SWING PIVOT lies from the current close. Geometry-free - the label owes nothing to a stop, target or horizon - which is what lets trade management be tuned separately instead of being baked into what the net learns. SwingPivotDirectionLabel reuses the ZigZag pivot the horizon and leg-size measurement already walk, so there is ONE notion of "pivot" in the codebase. It walks forward in time and stops at m_swingConfirmationBars: a pivot nearer than that is still repainting, so its label is not knowable yet and the bar stays Neutral. That boundary is the whole lookahead control for this target. TrainingTarget input is back (TARGET_BARRIER default, unchanged behaviour) with TARGET_FRACTAL and TARGET_SWING beside it; |TGT:SWG1 joins the fingerprint so switching trains a separate model rather than relabelling an existing one. ADZigZag was renamed to ZigZag throughout (30 identifiers). It has loaded MetaTrader's stock Examples\ZigZag at its stock defaults for some time - the migration was done, only the name was left behind, and a name that says "AD" about a stock indicator is exactly the legacy pointer this codebase should not carry. No behaviour change: same #resource, same params. Compile-verified in the staging copy: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1882f87451 |
feat(geometry): price every stop/target pair on the trades the model actually called
Step 1 of decoupling SL/TP from training. The geometry is currently chosen BEFORE the model exists - excursions -> stop at a quantile -> target at the policy minimum ratio -> labels -> the net learns those labels - so it has never been asked which pair maximises expectancy GIVEN WHAT THE MODEL CAN PREDICT. The scan meant to answer that reports "0 ELIGIBLE candidates" on this config (every rung disqualified by the close-all clamp), so nothing has ever compared the shipped pair to an alternative. This needs no retrain and no backtest. CFirstPassageLadder already stores the first-touch AGE of every rung on both sides and OutcomeR() resolves ANY pair exactly with the spread charged the way the fill charges it - so 14x14 pairs over one era's OOS calls is a few thousand array reads. - Expert/Training/GeometrySweep.mqh: CGeometrySweep accumulates (n, sumR, sumR^2, timeouts) per rung pair from the model's own directional OOS calls. Reads no chart, holds no net, opens no file - exercisable against a hand-built ladder, same doctrine as SDeployVerdict. - Best() ranks on the 3x3 NEIGHBOURHOOD mean, not the cell itself. A 14x14 grid read at its single highest cell is a best-of-196 maximum, biased upward by construction - the same selection problem the deploy gate corrects across eras. A pair whose neighbours also pay is a plateau; a lone spike is a lucky run of trades and does not survive the next window. GEOSWEEP_MIN_TRADES (30) keeps thin cells out of the selection entirely. - Wired into pass 3 where the call and the bar index are both in hand, reset per era, reported at pass-3 completion beside ReportCandidateGeometry. ONE line, and only when the recommendation CHANGES - it prints the shipped pair's expectancy and the best pair's on the SAME trades, so "better" is a difference rather than two numbers from two populations. Measurement only: nothing reads the recommendation yet and no geometry moves. Compile-verified in the staging copy: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d12b742a40 |
fix(deploy): print the selection score in the unit it is actually in
selectionScore used to be a win rate in percentage points and printed at one decimal everywhere. Under DeployOnExpectancy it is expected value in R, so "%.1f" rendered every real score as "0.0" - era 2's +0.05R and a genuine zero looked identical, which makes the journal useless for watching the ranking the plateau ladder is doing. One formatter, DeployScoreText(), next to the score it formats: "%.3fR" under expectancy, "%.1f%%" under significance. Routed all nine print sites through it (ensemble era line, best-so-far, panel, regression, new-best, era-cap prompts, the convergence line, the deploy dialog) and dropped the "%" suffixes they had hardcoded. No new prints, no new log lines. Compile-verified in the staging copy: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9883b209c7 |
feat(deploy): ship on positive EXPECTANCY, and let the chart draw before convergence
TWO CHANGES, both of which turn a permanent "nothing happens" into a decision.
1. THE DEPLOY GATE ASKS THE WRONG QUESTION. tradeable required the win rate to
clear chance by EDGE_MIN_SIGMAS - "can I PROVE an edge exists" from one OOS
window. On H4 that asks ~66% against a market supplying ~53%, so it is
unreachable by construction and no run has ever deployed through it.
SDeployVerdict now also carries the economics of the geometry actually being
traded - cost-adjusted break-even and reward:risk, both from the new
CostAdjustedGeometry() so a spread convention cannot be applied to one and
missed on the other - and derives
E[R] = (p - p*) * (1 + RR)
which is exactly zero at break-even by construction, so "profitable" and
"beats break-even" can never disagree. Under DeployOnExpectancy (new input,
default ON) tradeable becomes E[R] > 0 and selectionScore ranks eras by
expectancy instead of precision. Coverage and both-sides-live still gate
both: an expectancy over a handful of one-sided calls is not tradeable.
The struct also publishes scoreSE - the SE of selectionScore IN THE SCORE'S
OWN UNITS - because the score changes units with the objective (win-rate
points vs R). Both plateau bands now read it instead of precSE, which was
right for one objective and dimensionally wrong for the other.
Setting DeployOnExpectancy=false restores the previous behaviour exactly.
2. THE FILTERED VIEW COULD NOT DRAW WHILE ANY MODEL WAS TRAINING.
HistoricalNetVote built its divisor from VoteCapableWeight(), which answers
"may this member move real money" and returns 0.0 for an AI member until the
whole run converges. So the reconstruction's divisor was zero on EVERY bar,
every bar was skipped as "nobody looked", and the chart drew nothing at all -
for the entire training run, which before the plateau noise band was forever.
Reported as "no signals drawn since the refactor".
New ReconstructionWeight(): the same weight WITHOUT the converged-run
requirement, overridden on the AI member to ModuleWeight() gated on
SelfRanked() only. The overlay is a picture of what the vote WOULD have
shown, which a mid-training model can answer - the chart HUD already says so
with its "(trn)" marker. Live Direction() still uses VoteCapableWeight(), so
no untrained model gains a say in an order.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7075747f4a |
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cbd077c679 |
diag(training): report the window an era ACTUALLY trains on
With VerboseMode on, pass 1 reported eras of 422 / 949 / 1358 / 2562 bars on SP500 H4 - four models, same chart, same second - against a series holding ~16,264 bars, and the number moved every era (CONV: 2562, 3671, 3405, 3532, 2830). Nothing in the journal said so. ReportDetectability and the CAPACITY line both quote EstimatedInSampleBars, which is derived from the configuration and not from the era, so they kept reporting "11385 in-sample rows / OOS window 4874 bars" for a window that was a tenth of that. era.bars is MathMin(Bars(symbol, PERIOD_CURRENT, dtStudied, now) + historyBars, Bars(symbol, PERIOD_CURRENT)). A short era is therefore either a dtStudied that is too recent or a short price series, and those need opposite fixes - so the new line carries all three quantities plus the resolved dtStudied and SERIES_FIRSTDATE, not just the result. Reported on change only: an era over a warm feature cache runs in a fraction of a second here, and a per-era line would bury the journal. Diagnostic only - no training behaviour is changed by this commit. Compile-verified in the staging copy: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
906c60e227 |
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
Peer rows join m_isTrainQueue as NEGATIVE sentinels before the shuffle, so they interleave with this chart's samples instead of training in a block at one end. A block would be a curriculum: whatever the optimizer saw last would decide where it landed. TrainPoolStep is a separate path on purpose. Everything in pass 2's local branch after the forward pass reaches for something indexed by a LOCAL bar - m_labelCache, m_winLongCache, the excursion target, the arrow cache, m_Time - and a peer row has none of those. Sharing the path would mean inventing values for all of them, which is how another instrument's outcomes end up inside m_cumIsCorrect and the operating point gets fitted to them. The IS-vs-OOS gap is read as THE overfitting signal, so polluting the IS side would not crash anything; it would just quietly stop meaning what it says. The purge key reuses the label walk's own two bounds - the horizon and NextScheduledCloseAll - rather than approximating with a bar offset. A second horizon model here would drift from the real one, and this project already measured that the close-all, not the nominal horizon, is what actually terminates labels. Cutoff is the OLDEST OOS BAR'S TIME, in wall clock, because bar indices cannot be compared across instruments that each have their own calendar. Contribution happens while the window is still in TempData and before the forward pass overwrites it, and is gated to direction models: the meta head trains a different target on a wider input, which the fingerprint gate alone would NOT catch, since a meta model's fingerprint matches its own peers perfectly well. Use_Training_Pool ships false and does nothing until a second chart runs a matching fingerprint. Compile-verified against a BASELINE of the same tree without the wiring: both produce 12 errors, all error 313 invalid-resource-path from #resource directives that cannot resolve in a headless staged build (stock Controls res\*.bmp, plus the pre-existing Network.cl). Code errors 0, warnings 0, identical to baseline. Staging copy and junctions removed; the live .ex5 was never touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6974fb03af |
ditch(features): remove the eight dead feature groups from the input matrix
RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta, ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure, WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a closed verdict: the three oscillators are the same patterns that measured at chance as entries, and the Wyckoff family returned zero out-of-sample on five independent instruments - which is what closed the context score. RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks larger than it is. Every removed group contributed `flag ? N : 0` to the input width, and every flag was false, so the width was ALREADY zero for all eight: no .nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were hashed unconditionally and become literal 0 legacy slots (the convention the m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only when enabled, so their segments simply never appear - byte-identical to every fingerprint ever produced, since neither ever shipped on. CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted inside every .nnw, and Unflatten() rejects a size mismatch by falling back to constructor defaults - so dropping the dead fields would silently revert the tuned MA period of every model on disk while keeping its trained weights. That is the feature/weight mismatch this project has already paid for twice, and it is not worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing searches them. The class comment says all of this at the declaration. Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one indicator now, and a name saying "AD" for the MA handle is the kind of stale label that gets believed later. Its release-AFTER-recreate ordering is untouched - that is a documented fix, not bookkeeping. Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0 baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e366bb74ad |
refactor(config-lock): CConfigLock is a real collaborator, not a raw-include partial
AcquireConfigLock/ReleaseConfigLock moved off CExpertSignalAIBase into Expert/ConfigLock/CConfigLock, same view+adapter shape as BarrierHorizon/ExcursionHead. Stateful: m_configLockName is exclusive (grep-verified, nothing outside Lifecycle.mqh's old body touched it). Pure relocation - same FNV-1a hash, same owner-liveness check, same log wording. Left uncommitted mid-campaign; independently compile-verified in isolation now (0 errors/0 warnings) before this commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a2c965879e |
refactor(inference): dedupe the 3-class strict-majority argmax test
ApplyClassificationSoftmax/AdjustedSignalFromSoftmax/DirectionalMargin each re-derived `pBuy > pSell && pBuy > pNeutral` (and the Sell mirror) independently, one of them documenting the duplication by comment rather than eliminating it. Added Argmax3() as the single derivation (ties to Neutral); all three now branch on its ENUM_SIGNAL result instead of re-testing the comparison. Pure relocation, statement-by-statement equivalent - verified by diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
b95ee72f4d |
refactor(labels): split the close-all budget / horizon ladder into CBarrierHorizon
Expert/AIBase/Labels.mqh (1807 lines) exclusivity-grepped almost entirely SHARED: the label/win/excursion/ladder caches and the geometry-derivation/ prebuild state are touched with real per-bar array logic by Training.mqh's hot era loop (m_labelCacheBuy/Sell/HasValue at 22+ sites), by FeatureScreen.mqh's geometry scan (direct writes to m_barrierScanSlMult/TpMult, m_geometryAdopted, m_geometryCfgSaved), by AutoTune.mqh and by SignalMETA.mqh - moving that state into a collaborator would mean wrapping dense hot-loop array indexing behind method calls across 5 files for no coupling reduction (same judgment already recorded for AutoTune.mqh's remainder / Inference.mqh). One genuinely closed sub-cluster survived the grep: the scheduled close-all budget and the horizon ladder snap (NextScheduledCloseAll, MeasureCloseAllBudget, EffectiveHorizonMax, RequiredHorizonBars, SnapHorizonToLadder, GrantedHorizonBars). Only 2 fields are exclusive (m_closeAllCycleBars/m_closeAllMeanBudget - grep- verified, Lifecycle.mqh's touch was constructor-init-list only) and NONE of the 6 methods has any external caller outside Labels.mqh (grep-verified whole-repo), so nothing needed rewiring. New Expert/BarrierHorizon/: IBarrierHorizonView.mqh (abstract, 4 accessors, 3 reused from the signal's existing Chart* getters, 1 new HorizonSwingMedianBars() wrapper) + AIBaseBarrierHorizonView.mqh/ AIBaseBarrierHorizonViewImpl.mqh (the adapter) + BarrierHorizon.mqh (CBarrierHorizon, STATEFUL - owns the 2 exclusive fields as real members). Every method body is a verbatim relocation (diffed programmatically against git HEAD modulo the field-> view substitutions - identical except one comment-wording update). The original 6 declarations on CExpertSignalAIBase became one-line forwards at their existing position; Labels.mqh's own callers of these six needed zero changes since they call them unqualified, which now resolves through the forwards. Labels.mqh: 1807 -> 1643 lines. The rest of the file (label-cache population, TripleBarrierLabel, DeriveBarrierGeometry, StartLabelCachePrebuild/ AdvanceLabelCachePrebuild, exit-policy simulation) is deliberately left as a raw-include partial - not separable without relocating Training.mqh's era-loop coupling, not reducing it. Self-compiled 0 errors, 0 warnings (_claude_stage, ~94s). |
||
|
|
4dede6f6db |
refactor(features): FeatureBuilder is a real collaborator, not a raw-include partial
Expert/AIBase/Features.mqh (2017 lines, 38 methods) split by exclusivity grep (whole-repo, not just Expert/): 30 methods -> Expert/Features/FeatureBuilder.mqh (CFeatureBuilder + CFeaturesView/CAIBaseFeaturesView), 8 stay behind as a much smaller raw partial. CFeatureBuilder is STATEFUL, same shape as Excursion/OnlineLearning: owns the 10 feature-only indicator handles (m_Volumes/m_MA/m_RSI/m_MACDFeature/ m_Ichimoku/5 AD* CiCustom indicators - grep-verified touched nowhere else in the repo, only their bare declarations) plus the depth-probe/handle-repair/ spread-series/detectability-latch scalars (exclusive, Lifecycle.mqh ctor-init only elsewhere). m_Open/m_Close/m_High/m_Low/m_Time/m_ATR/m_ADZigZag stay signal-owned - Labels.mqh/AutoTune.mqh/Training.mqh read them directly - and are reached read-only through the view (FeatureOpenAt/FeatureHighAt/ FeatureLowAt/ChartBarClose/ChartBarTime/OnlineAtrMain, all reused where a forward already existed). Deliberately did NOT move InitOpen/InitClose/InitHigh/InitLow/InitTime/ InitADZigZag/ResizeBuffers/RefreshData: they manage the 7 shared indicators' Create/BufferResize/Refresh lifecycle, which would need a pure-relay wrapper per operation per indicator for zero coupling benefit - same judgment as Topology's boot sequence. They stay in Expert/AIBase/Features.mqh and reach CFeatureBuilder's 10 owned indicators through 20 new Feature*BufferResize()/ Feature*Refresh() forwards (signal calling into its own owned collaborator directly, no view needed in that direction). Whole-repo grep (not just Expert/) caught a real external miss the campaign's own doctrine warns about: Signals/SignalMETA.mqh read m_spreadSeries/ m_spreadSeriesBars directly as an inherited protected field (a subclass, not an AIBase/*.mqh partial) - fixed with two new FeatureSpreadSeriesBars()/ FeatureSpreadSeriesAt() forwards. Verified: if(/for(/while( counts identical between the original file and the new split (269/20/1); return-count delta (+12) fully accounted for by the 12 new trivial one-line forwards added (10 indicator BufferResize + 2 spread- series getters); quoted-string-literal diff empty except two doc-comment paraphrases. Self-compiled 0 errors, 0 warnings. |