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> |
||
|
|
8083a31754 |
diag(gate): move the conviction curve to the horizon that has value, and add mean-d per rung
The 5-bar conviction curve cannot answer the question it was built for. The oracle measures ~0 at 5 bars across three charts (+0.012, -0.054, +0.064), so PERFECT foresight earns nothing there and no rung can show payoff either. Every reading it produced was null by construction. It was placed at 5 bars for statistical power, before the oracle showed what that horizon is worth. Kept as a control; the hold-horizon curve is the one to read. Also adds MEAN DISTANCE-TO-PIVOT PER RUNG, which is the high-power form of the same question. Payoff falls ~0.34 ATR for every bar of distance to the pivot (fleet-pooled: d=1 +2.095, d=2 +1.743, d=3 +1.300, d=4 +0.969, d=5 +0.769, wrong calls -0.668). So a rung that selects NEARER pivots is worth more per call even at unchanged precision - and mean-d is a far tighter statistic than mean-payoff, because d spans five bars where payoff spans several ATR. That matters because it can REOPEN a lever I closed. Precision does not rise with the rung - every 15-vs-10 comparison across six charts sits below 0.71 sigma - so the threshold looked exhausted. But precision is not the only thing a threshold can select for. If conviction correlates with proximity to the pivot, raising it buys payoff without buying precision. Directional labels only: an incorrect call has no pivot and therefore no distance, and folding those in as zero would read as "this rung picks pivots that are imminent" when it means the opposite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0d9320cc87 |
diag(gate): the ORACLE - what a perfect caller of this label would earn
The ceiling on the target, and the measurement that decides where the work goes. Same payoff arithmetic, signed by the LABEL's direction instead of the vote's, over every directionally-labelled shared bar. If a model that got EVERY pivot right still earns nothing over the holding horizon then the target carries no money and no amount of model improvement reaches any - the label, not the network, is what has to change. If the oracle earns well the target is sound and the shortfall is the model's. Those are completely different programmes and nothing so far distinguishes them. It uses no forecast, so it is not a leak: it is the value of perfect foresight OF THIS LABEL, reported as a benchmark. Nothing may trade on it. Accumulated above the voter and direction-policy filters, like the zero-skill book, because it is a property of the bars and their labels rather than of what the vote did with them. A bar with no directional label offers a perfect caller nothing to take and is skipped rather than counted as zero - the benchmark is "every call it COULD make". Motivated by the first skill-by-distance row, which already reframes the day: correct calls earn +0.75 to +1.90 ATR against a spread of 0.005-0.042, and incorrect ones cost -0.66. That puts break-even precision near 32% against a measured 33-37% - thin, but on the right side, and utterly unlike the "no payoff" reading the confounded 5-bar window suggested. 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> |
||
|
|
3372b82dfa |
diag(gate): the conviction curve - does payoff rise with vote magnitude?
The practical question behind "can I just trade the strongest signals" is whether payoff rises with vote magnitude. The threshold sweep already visits every rung, so the whole curve costs four arrays and no extra pass. Reported as the DRIFT-FREE statistic per rung - long plus short, both sign corrected - with the two halves alongside. The halves alone invite reading a drift-fed long side as skill, which is exactly the error the zero-skill book caught at the certified rung: an always-long book earns MORE than the vote on two of three charts. Taken at the SHORT horizon, which is the one with the power. Pooled across the three training charts the certified rung reads -0.012 +/- 0.074 ATR - a tight null, 95% interval [-0.16, +0.13], with the long/short pattern (+0.030 against -0.041) being the drift signature exactly. The hold horizon agrees and is 3.7x noisier, so the answer is not a horizon artifact. Precision is already known not to rise significantly with the rung (every 15-vs-10 comparison across six charts sits below 0.71 sigma). If payoff rises anyway that is a surprise worth having; if it does not, the two agree and the threshold lever is closed on both counts. Still gates nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
feaadd80a2 |
diag(gate): split the payoff by side at the horizon that can actually resolve it
The by-side test is the one that separates directional skill from drift, but at
the HOLD horizon it cannot answer: payoff overlap is the horizon itself, so an
18-bar window leaves ~65 independent observations per chart and a standard error
of 0.25-0.45 ATR against an effect that would matter at 0.1.
The 5-bar window carries ~3.8x the independent observations and roughly half the
standard error. It buys that power by risking a window that ends before the
pivot has committed - which is exactly why the horizon was widened in
|
||
|
|
ce4f74fe2c |
diag(ensemble): measure how much the four members actually disagree
The ensemble beats its best single member by +2.2 to +6.8pp on all six charts - sign-stable across six instruments, so the ensemble is doing real work rather than diluting. How much MORE is available depends entirely on how decorrelated the members are: the variance of an m-member average scales as (1+(m-1)r)/m, so at r=0.8 four models are worth about 1.2 independent ones and at r=0.3 nearly 3. Nothing measured that, so the obvious next lever - different feature subsets per member, or a fifth architecture - could not be costed. Both force a full retrain of 24 models, which is not a price to pay on a guess. Measured on the SIGNED VOTE, which is what actually gets averaged: not accuracy, not raw confidence. Two members can agree on direction almost always and still contribute independently through magnitude. Accumulated over every SHARED row rather than fired ones - restricting to fired rows would measure agreement only where the members already agreed enough to fire, which is the sample most biased toward agreement. A member whose signed vote never varies (all abstentions, a dead tier) is SKIPPED rather than counted as r=0, which would drag the mean toward "decorrelated" using a member carrying no information at all. Reported as an effective member count, which is the honest way to say what four models are worth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7500e08e17 |
feat(gate): the payoff number needed a zero-skill book and a by-side split
payoff-v1 reported what a call was worth and nothing to compare it against. A
positive mean R is not a finding on its own: if the instrument drifts, an
ALWAYS-LONG book earns a positive mean too, and drift is the one anomaly family
this project has found that survives cost - so the vote would be reporting the
market's own move as if it were its own.
Two comparisons, and the second is the one that decides it:
ZERO-SKILL BOOK - the same forward move accumulated with a fixed long sign over
every SHARED row, not only fired ones. Accumulated above the voter and
direction-policy filters deliberately: restricting it to bars the vote fired on
would compare the vote against a baseline the vote itself selected. Always-short
is exactly its negative, so one pass covers both.
BY SIDE - the vote's own payoff split by the direction it took, still sign
corrected, at the rung the live signal is actually trading:
both sides positive -> directional skill, it pays going either way
one positive, one negative
and roughly cancelling -> it found the drift, and the pooled mean is
saying nothing about skill
This is drift-free BY CONSTRUCTION - drift enters both sides with opposite sign
after the correction, so it cannot manufacture a two-sided positive. That is
precisely what a pooled mean cannot tell you and what no baseline subtraction
fully recovers.
The split is taken at the CHECKPOINTED rung, not this era's derived one: the
derived rung is not known until after the row loop that accumulates the split,
and the checkpointed rung is the operating point the question is actually about.
Still gates nothing.
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> |
||
|
|
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 ( |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6adb710a79 | fix(binomial): correct tail calculation in BinomialUpperTailP and add tests for accuracy | ||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 (
|
||
|
|
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> |
||
|
|
0cd20a5749 |
diag(geometry): the sweep must not present "least negative" as a recommendation
First 35 eras across both charts, this run:
shipped 1.21/2.43 (SP500) and 1.26/2.52 (USDJPY): mean -0.0525R,
positive in 6 of 35 eras
best plateau after the neighbourhood guard: mean +0.0292R,
positive in only 17 of 35
most-recommended pair: 20.00/0.50, seven times - a ~40:1 lottery that is
simply the least negative cell in an all-negative grid
The recommendation jumps between opposite corners of the ladder between
consecutive eras, which is a grid fitting noise rather than a geometry worth
adopting. Two changes so the line cannot be misread:
- GEOSWEEP_MAX_TIMEOUT_SHARE (0.70): a cell where most trades never touch
EITHER barrier is not a geometry being tested, it is the horizon close being
measured. 20.00/20.00 timed out on 100% of trades and was still selected.
Excluded from SELECTION only; the cell stays filled and readable.
- When the winning plateau is <= 0 the line now says so in those words:
"NOTHING ON THE LADDER PAYS ... the pair below is the LEAST NEGATIVE cell,
not an edge."
Still measurement only - nothing reads the recommendation and no geometry moves.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
c14ffc84e2 |
fix(training): a yielded pass is not a finished pass - Train() must return
Every era was a ~1,200-bar chunk of a 16,264-bar window, and the oldest 90% of
the history was never reached.
All four passes yield mid-chunk on the 120ms budget: each one calls
StashEraResume (the single writer of m_eraResumePending) and returns. Those
used to be returns from Train() itself. When the passes were extracted into
their own methods (
|
||
|
|
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> |
||
|
|
9c31625aae |
fix(training-pool): say why a peer was rejected instead of adopting nothing in silence
Two charts (SP500 H4 + USDJPY H4) ran with the pool enabled and produced no TrainPool directory, no adopted rows and not one journal line. The pool was inert and there was no way to tell that from "the feature is off". It could never have fired: the fingerprint is not symbol-invariant. It hashes NeuronsCount, which counts the alt-data columns - and those are per-symbol (SP500 carries cot_spec_net, the FX majors cot_idx_1y/3y/chg_4w) - and the cross-asset block appends ":IDX2" when base currency == profit currency, true of an index and false of a pair. SP500 came out 50 features wide under XA:6:IDX2, USDJPY 52 wide under XA:6. Compatible() gates on both, so adoption was zero by construction. - STrainPoolHeader::MismatchReason() replaces the bare Compatible() predicate and names the mismatch; Compatible() now delegates to it, so "may I adopt" and "why not" can never drift apart. - CTrainPoolReader::Adopt() reports its own verdict - adopted, alone, or every peer rejected with the reason per file - and reports it on CHANGE only. An era over a warm feature cache runs in a fraction of a second here, so a per-era line would bury the journal. The duplicate Print in RunPass2 is gone; pool state is now reported from exactly one place. - CTrainPoolWriter::Publish() rate-limits to TRAINPOOL_MIN_PUBLISH_SEC (300s). Every era re-derives the same rows from the same in-sample span, so per-era publishing rewrote a multi-megabyte file continuously for no new information. The first publish is never delayed. Compile-verified in the staging copy: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
78a070eb5c |
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\: IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/ AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning). STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS continual-learning simulation state and the pattern-database backfill state are genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/ Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at era/lifecycle boundaries, never owned it, so it moved onto the collaborator as real members (same doctrine as Excursion). Those external touch points became consolidated view/forward calls instead of raw field pokes - AbortSimIfActive() replaces THREE separate copies of the same delete/null/false triple (Training.mqh's stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five- field reset block, DeployNet() replaces the shadow-preferred net selection duplicated in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end blend Training.mqh used to poke m_shadowNet for directly. Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.); added ~30 new Online*() wrappers only for what nothing else exposed yet. The three PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning member instead of touching the field directly - CModelPersistence is unaffected. Every method body is a pure relocation of the original's statements in original order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff against the pre-extraction file kept in the working tree until this commit. Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
3278ea4ae2 |
refactor(excursion): ExcursionHead is a real collaborator, not a raw-include partial (S4)
Expert/AIBase/Excursion.mqh was 13 method bodies of CExpertSignalAIBase, #include'd after its declaration - same "not a module" problem already fixed for ChartUI (S2) and Persistence (S3). Extracted to Expert/Excursion/CExcursionHead behind CExcursionHeadView/ CAIBaseExcursionHeadView, same view+adapter shape. STATEFUL, unlike Persistence (0 exclusive fields): grep-verified 26 fields (m_excNet and every accumulator/trailing-ring field) touched nowhere else in Expert\ except Lifecycle.mqh's old ctor-init-list defaults and destructor deletes (now moved onto CExcursionHead's own ctor/dtor). m_geo (SGeometryScan) and m_ladder (CFirstPassageLadder) stay on the signal - both are genuinely shared with Labels.mqh/ Training.mqh at era boundaries - and are reached only through 15 new Exc*() view wrappers, including one consolidated ExcGeometryScanAccumulate() call (same doctrine as Persistence's RunCpuInferenceSelfCheck) rather than field-by-field pokes. All 13 original public methods stay at their same declaration point as one-line forwards to m_excursionHead. Training.mqh's 2 raw m_excUs reads now go through the new ExcursionMicroseconds() forward. Every method body is a pure relocation, verified statement-by-statement against the original (git show HEAD~1:Expert/AIBase/Excursion.mqh). Compile-verified: 0 errors, 0 warnings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
053d704a84 |
refactor(chart): ChartUI is a real collaborator, not a raw-include partial (S2)
Expert/AIBase/ChartUI.mqh was 869 lines of method bodies of CExpertSignalAIBase, #included after the class declaration - free to touch any of its ~500 members. First of the eleven AIBase/*.mqh partials to come out (fewest inbound edges - see the SOLID campaign session order), using the same view+adapter shape already proven for CTrainingDataView. CChartView (Expert/Chart/IChartView.mqh) is the abstract read/behaviour surface a chart-rendering collaborator needs - identity, bar/model access, the prediction cache, and the training/vote/meta scalars the panel and HUD line summarise. CAIBaseChartView is the adapter the signal owns and binds to itself (MQL5 gives a class exactly one base, so CExpertSignalAIBase cannot implement the view directly). CChartUI is the real collaborator: it owns the arrow-restore queue, the rescan queue/tally, the last-arrows-saved count and the purge-mismatch latch as its own fields (verified via grep to be touched nowhere else in Expert/), and reaches everything else - including StartChartSignalRescan, moved in from its old inline home in the header since it drives the exact same rescan state machine AdvanceChartSignalRescan drains - through the view. m_arrowSignalCache and m_signalClusterWindow stay on the signal: Training.mqh writes the cache directly every era and the training-data view already reads it, so moving it would mean rewriting Training.mqh's write sites too - out of scope here. CChartUI reaches it through four bounds-checked accessors instead of a raw member poke. All 10 public methods keep their exact signatures and become one-line forwards on the signal, so no other file's call sites change except Training.mqh's one era-end status refresh, which now reads RefreshStatusLabel() rather than reaching into CChartUI's now-private last-displayed-neuron cache directly. Verified structurally, not compiled (never compile - the operator does, in MetaEditor): brace balance checked on every touched/new file against HEAD, and the view/adapter/impl method lists cross-diffed to confirm all 59 accessors match 1:1 across the interface, the adapter declaration and the adapter body. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
8c64ec7018 |
refactor(meta): the signal tree owns its gate - no global
g_warriorMetaGate was a file-scope mutable pointer, and it did not need to be. The root CExpertSignalCustom - the one CExpert actually calls CheckOpenLong/Short on - now holds the gate as a member, and children reach it through a parent back-pointer AddFilter sets on adoption. That was the last piece of the meta veto that behaved like ambient state: - CheckOpenPosition reads MetaGate() instead of a global. - EnsembleEraVerdict's replay reads the same MetaGate(). It sits deep in the training code inside an AI filter and had no route up the tree; a global WAS that route. m_parentSignal is now, and a back-pointer is safe for the same reason the gate adapter's owner pointer is - m_filters and m_gates free their children, so a parent always outlives them. - The stale-pointer hazard is gone by construction. The global had to be hand-cleared at every re-init because an input change re-enters OnInit in the same program instance and frees the old head; the root signal is new'd fresh each time, so nothing survives one. That reset line is deleted, not moved. Note what did NOT need doing: the tree already owned the meta head itself. AddFilter routes non-voters into m_gates, so it has been a gate child of the root since the S3 wiring - it was only the VETO that lived outside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
303c9bf412 |
refactor(meta): the era's candidates are an object, not eight base members
Five parallel arrays, a count and a two-array intrusive chain sat on
CExpertSignalAIBase - inherited by every direction model, filled and read
by exactly one subclass. CMetaCandidateStore takes all eight.
What that fixes beyond the clutter:
- THE CHAIN WAS LINKED BY HAND. MetaPrepareEra wrote next[id] = head[bar]
then head[bar] = id itself, after six ArrayResize calls it also wrote out
itself. Add() does the linking, Reset() does the sizing, and a bar off
the grid now cannot be stored at all rather than stored unreachable.
- THE BOUNDS TEST HAD FOUR SITES AND THREE IMPLEMENTATIONS.
MetaCandidateWon indexed side[] with no test at all and answered
"short" for any id out of range - the same shape as the ladder's
negative-index read (
|
||
|
|
3d2ee517ca |
refactor(meta): the veto is a gate, not a virtual every signal carries
Since S3 (
|
||
|
|
11006a8e38 |
fix(train): BeginTrainRun read Train()'s parameter from a scope it no longer had
The run-start block calls TrainWindowStart(StartTrainBar), and StartTrainBar is Train()'s parameter. Moving the block into its own method left the read behind. Now passed explicitly. THIRD TIME THIS FAMILY HAS BILLED THIS SESSION, and the third distinct sub-shape: |
||
|
|
4e508460ac |
refactor(train): Train() is the era lifecycle again, not the whole of it
Train() was 1,273 lines. It is now 79, of which about 35 are statements, and they read as what the function is: preempt, begin run, begin era, four passes, advance, complete, report, finalize. Seven methods carry what left it: TrainCallPreempted 107 six ways this call is not a training call at all BeginTrainRun 130 once per run - history sync, window, one-shot walks BeginEra 232 once per era, or resume a chunk that yielded ReportPass1Outcome 105 what pass 1 found, said out loud AdvanceEra 68 count the era, decide whether the RUN ends CompleteEra 590 calibrate, gate, rank, checkpoint, ladders, persist ReportBarrierHold 62 why this member is idle at the era barrier ClaimCallForWalk 15 the preamble the three exclusive walks shared TWO DRY FIXES fell out rather than being looked for. The three exclusive walks each had to tell TWO watchdogs the same thing - the stall reporter which branch is running, the era-barrier watchdog that this member is BUSY rather than stuck - written out three times, so a fourth walk was three chances to be added with only one of them. And the barrier-hold reporting was 44 lines inline in a branch whose only other statement was resetting a tick. CompleteEra is lifted WHOLE and stays that way for now. Its parts share thirty-odd locals - the recalls, the gate verdict, the better/worse flags - and threading those through three signatures would recreate exactly the eight-locals-across-four-passes problem STrainEra was built to end. Splitting it needs an era-outcome object first, not more parameters. VERIFIED AS A PURE MOVE: statement multisets, old file vs new, differ only by the 14 `return;` that became 17 `return true;` plus 3 new returns at the call sites, the 3 collapsed walk preambles, the 8 new signatures and their braces. Nothing else moved, and every function closes at depth 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d7469c69a1 |
fix(gate): a field renamed on the definition side left one reader behind
SDeployVerdict's bothSidesLive became twoSided when the member and ensemble
gates were unified, and the member call site kept reading gate.bothSidesLive.
SECOND TIME THIS EXACT SHAPE HAS BILLED THIS SESSION - the first was `s == 0`
surviving the deletion of the loop that declared `s` (
|
||
|
|
3ea2bbc015 |
refactor(gate): the member gate and the ensemble gate were one rule written twice
SDeployVerdict::EvaluateRates() is now the deploy arithmetic - coverage
floor, chance + EDGE_MIN_SIGMAS x SE, tradeability, and the coverage-
discounted ranking score - and both gates call it.
The duplicate was self-documenting. The ensemble copy carried three comments
asking a reader to keep it in step with the member copy by hand: "same
intent as the member gate's coverage floor + bothSidesLive", "the two gates
have to apply the identical correction or the ensemble becomes the easier
one to clear", "same lexicographic ordering as isBetterEra". They had
already fallen out of step once -
|
||
|
|
93d7bbe677 |
refactor(oos): twenty-one counters with one lifetime become one object
SOosTally holds this era's OOS confusion counts and the rates they imply.
The signal keeps one member where it kept twenty-one, and the era-reset
block loses twenty of its twenty-one clearing lines.
THE SHAPE THIS ENDS is the one that produced
|