forked from animatedread/Warrior_EA
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
980f10b60c |
feat(label): ride the leg, exit on the flip - the leg-ride target replaces the pivot-event target
The pivot label paid +1.6 ATR on a hit and -1.7 on a miss at 50-65% precision: a zero book by arithmetic, because a "bottom" that is not one is a move that kept going, and the model called it because a big move had just happened. This target never asks for a turn. Direction is the leg in progress, known on the bar; the label is whether riding it from here until the leg flips pays at least LEG_LABEL_MIN_RIDE_ATR (1.0). The flip is the exit the EA now places. - Labeling/LegState.mqh: a line-for-line replica of ZigZag.mq5 (12/5/3) run over bars <= t only, so "which leg am I in" is what the chart showed on t, never the final buffer. Verified against the real indicator at every init (LEG STATE REPLICA line) and by Tests/Test_LegState.mq5 on hand-built bars. - LegRideLabel (Labels.mqh): ride = legDir * (close[flip] - close[t]) / ATR, the flip found by asking the same as-of function of each newer bar in turn. Unresolved until the leg has flipped inside loaded history. Lifespan = the leg, so EffectiveSampleSize deflates honestly (~3x fewer than the window constant claimed); the topology's overlap is the median leg again. - Money weight = |ride|. Online step's exit bar = the flip. - Three as-of leg features in the swing block (direction, extension in ATR, age); FMASK:2 keeps them. Fingerprint TGT:LEG1:10 - RETRAIN-FORCING. - Era verdict: the book WarriorRungBookProfitable gates is the RIDE (entry at the call, exit at the flip), printed per rung and at the certified rung beside ALWAYS-RIDE (zero skill) and the ORACLE RIDE (the ceiling). Fixed- horizon payoff stays as the signal diagnostic. The by-distance profile, its slot arithmetic and the reversal-exit pass are deleted (measured: the vote's reversals land 33-137 bars late; dead). - Live: Exit_On_Leg_Flip (default on, not in the fingerprint) closes when the as-of leg flips against the position and places no take-profit; the measured stop stays. Build tag leg-ride-1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
635e4396ab |
feat(training): measure the reversal-exit book - what each call earns held until the vote flips
A perfect pivot caller buys the bottom and sells the top and collects the whole leg: median 4.5-5.0 ATR on these charts (SWING LEG SIZE line at init). The era report's fixed 18-bar exit collects ~35% of that and a mean-excursion target about half, so the exit - not the label - is where the leg is left. Nothing measured the one exit that can keep it. Every era now walks the certified rung's fired calls in time order and scores each one held until the vote next fires the other way - no stop, no target - in ATR at entry, by side, with the mean hold in bars, beside the fixed-horizon exit on the SAME calls. Calls with no reversal before the window ends are dropped like the leading edge; same-side re-fires are ignored. Two per-row fields (close, ATR) join the OOS vote store to make an arbitrary row-to-row payoff measurable. Reported only, gates nothing, retrain-neutral. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
39e4bb587a |
refactor(signal): delete the barrier sweep, the automatic direction policy and the pivot-geometry features
Three layers out, ~1,000 lines, RETRAIN-NEUTRAL - the fingerprint and the emitted feature vector are byte-identical, and a WSTC .stats still restores under the new WSTD reader. BARRIER SWEEP. The 64-cell (stop, target) grid, its 999-draw Westfall-Young permutation null, the first-touch offsets recorded per OOS row, the swept-pair persistence (WSTA) and the tier-1 branch of ResolveBarrierMultiplier are gone. It never cleared its own null on any chart (p 0.19 to 1.00), so it gated nothing and cost a grid plus 999 rescans per era. Live stops and targets come from the mean excursions at the hold horizon, as they did in practice. DIRECTION IS THE INPUT. WarriorEffectiveDirection() returns tradingdirection. The bar-body drift screen (Signal\DriftScreen.mqh) and the 2-SE by-side expectancy test that outranked it are removed with their unfiltered accumulator and persistence (WSTB/WSTC). On a fleet where no side clears 2 SE on payoff the test could never speak - SP500 and XAUUSD never once printed its verdict across two days of logs - so the screen decided by default. The era verdict now certifies BOTH sides whatever the input says: the checkpoint no longer depends on a per-chart input, so one trained model serves LONG_ONLY, SHORT_ONLY and BOTH and the input can be optimized in the tester without a retrain. The blocked side's vote still closes a position under Exit_On_Reversal_Vote. PIVOT GEOMETRY. The five confirmed-pivot swing features (leg direction, distance since pivot, prior-leg magnitude, retracement ratio, bars since pivot) scored 4/2/0/2/1 of 24 on the keep-screen and the mask had already dropped them from every emitted vector. Deleted from the builder; the kept trend-position columns are swing[0..3] now and the vector is unchanged. SWING LEG SIZE, measured. MeasureSwingGeometry now prints the leg-size distribution in ATR at the leg's start pivot. Offline on the archived H4 feeds (stock ZigZag 12/5/3, ATR 20): median leg 4.5-5.0 ATR, p90 9-10, max 35-76; median length 13-14 bars, a third of legs outlast the 18-bar hold. The era report's ORACLE (+1.6 ATR) is a fixed-horizon close-to-close capture from the call bar, not the leg - a perfect caller entering at the pivot and holding 18 bars earns ~1.9-2.0 ATR on the same feeds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3422f1b8f4 |
fix(barriers): Sidak was inert on this grid - permutation null instead
Sidak's correction is exact only under INDEPENDENCE. The 64 barrier cells
are built from the same first-touch offsets on the same rows: stop 2.5 /
target 3.5 and stop 2.5 / target 4.0 are almost the same trade. Correcting
64 heavily overlapping tests as 64 independent ones does not merely
over-penalise, it removes the test's power entirely.
Measured, 200 synthetic trials at 400 rows with the correlation structure
of real excursions:
true null permutation mean p 0.496 (uniform -> 0.500)
P(p<0.05) 0.030 vs nominal 0.050 - calibrated, and
erring conservative
edge 0.10 permutation rejects 82%, SIDAK 0%
edge 0.20 permutation 100%, SIDAK 0%
Sidak's mean p on this family is 1.000. It would have refused a real
barrier edge of any size we could plausibly have had - which is what was
rejecting SP500 at p=0.0519, the one chart in the fleet with positive
excess over its own zero-skill book, whose swept pair is worth +0.426 ATR
per call against a mean-excursion fallback (stop 2.60 / target 2.58) that
is a coin flip by construction since MFE ~ MAE fleet-wide.
- BarrierPermutationP: Westfall-Young maxT. Each draw flips every row's
DIRECTION on a fair coin and rescores the whole grid; p = share of draws
whose maximum z beats the observed maximum. Direction is what the claim
is about, so direction is what gets permuted - the touch offsets, the
volatility clustering they encode, the row population and the grid are
all held fixed.
- A sign flip SWAPS which barrier is target and which is stop; it does not
negate the outcome, because the tie rule and the still-open case are both
asymmetric. Both direction vectors are therefore built through the same
shared WarriorBarrierCellOutcome, extracted here so the rule exists once -
a null resolving ties even slightly differently would be testing a
different quantity than the one it judges.
- The null's row gate is verified against the grid's own row count at
runtime and REFUSES (falling back to Sidak) if they differ. That
divergence is silent by nature: both halves look correct alone and only
the p is wrong.
- 999 draws. Monte-Carlo SE at p~0.05 is 0.0069 and is PRINTED with the p;
a verdict within 2 SE of the cutoff is labelled a coin flip in the log
rather than reported as a finding.
- Both direction vectors are precomputed once per era, so a draw is 64
additions from a flat array rather than 64 touch lookups plus a call.
That is what makes 999 draws affordable inside an era report.
- Falls back to Sidak, never to "no correction", if the null cannot be
built or an unload interrupts it.
- The deploy gate's own best-of-N test is untouched.
Compiled 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6f53809713 |
fix(barriers): the swept pair was fitted to a rung the era did not certify
The 64-cell grid accumulated under `if(s == derivedIdxPrev ...)` - the rung the LIVE signal is currently trading - because this era's derivedIdx is not known until the row loop has finished. But the pair that wins that grid is persisted to .stats and becomes the stop and target the chart actually trades, and it was reported beside a payoff and precision measured at derivedIdx. Those are different rungs whenever the operating point moves, which is measured at 6-34% of eras. The only symptom was an n printed beside a different n, which is why it survived review: EURUSD reported a swept n of 2048 against 1470 fired, USDJPY 1767 against 1208. The other four charts happened to agree that era and looked correct. - barSum/barSumSq/barN widened to [ENS_THRESHOLD_SWEEP_N * 64], accumulated at EVERY rung, read out at derivedIdx once it exists. - NOT a best-of-384. Every rung is accumulated but exactly one is scored; the other five are discarded unread. Nothing selects across rungs, so the family the p-value must cover is still the 64 cells of one grid. - The distance buckets STAY at derivedIdxPrev. They are a report - nothing reads them back - so the rung the operator is currently trading is the right one for them. Only the barrier grid feeds a persisted decision. - Cost is integer compares on first-touch offsets already recorded by the payoff walk; no prices are read. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d82e7b8ed5 |
feat(training): money-weighted loss - cross-entropy could not see magnitude
The objective was never aligned with the book. Cross-entropy against the pivot label scores a 0.2 ATR pivot and a 3 ATR pivot identically, so the network optimises how OFTEN it is right and nothing tells it that being wrong on the big ones is what kills the P&L. That is exactly the measured pathology: precision 50.4-65.2% against a 13.4-14.5% base rate (23-52 sigma, unambiguous) while the book earns +0.002 to +0.045 ATR per call and loses to always-long on five of six charts. The label is NOT the problem. The oracle - a perfect caller of this exact label - earns +1.167 ATR @5 and +1.627 @19 over 3,986 calls. The target is rich and we capture ~2% of it. Per-sample weight = |forward return at the hold horizon| / mean(|r|), clamped to [0.25, 4.00]. Normalised by the mean, so an era trains on the same TOTAL gradient mass as before - redistributed from cheap bars to expensive ones - and the effective learning rate does not move. - Variables/MoneyWeight.mqh: the knob (MONEY_WEIGHT_VERSION 0 restores unweighted training and the pre-change fingerprint exactly), the transform, the fingerprint tag. Runtime predicate, not #if - MQL5 has no #if <expression>. - The floor is load-bearing: sampleWeight = 0 IS NOT A SKIP in this optimiser (Adam momentum and weight decay still apply, t and m_batchCount still advance), so weights must never decay toward zero. - Cached as the RAW |r| under the label's own validity flag, normalised at use: the mean keeps moving as bars resolve, so a cached weight would be stale for every bar but the last. - Peer pool rows stay at 1.0 - the pool record carries no forward return and deliberately does not name its source bar. Normalisation is what keeps the local:peer gradient ratio unchanged. - Not lookahead: measured from future bars exactly like the label, reaches the loss only, and has no route into the feature vector. - RETRAIN-FORCING by design. A weighted and an unweighted model have identical topology and identical weight-file shape and differ only in what they were taught to value; the fingerprint is the only thing that can tell them apart. - Reports the realised weight distribution once per cache, including the share pinned at each clamp - a saturated scheme is invisible in every downstream number. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
724f8dce83 |
feat(features): fleet keep mask - 13 of 49 columns, the false-pivot lever
RETRAIN-FORCING. Re-keys every fingerprint (column count 49 -> 13, plus an explicit |FMASK: token). Decomposing the deployed fleet's payoff by distance-to-pivot says the whole edge is one number. A hit pays +0.94 to +1.57 ATR and is FLAT across d=-3..0; a miss costs -0.76 to -1.62; the miss rate is 34.5% to 49.4%. Remove the misses and every chart earns +0.97 to +1.22 instead of +0.05 to +0.32. One percentage point off that rate is worth +0.011 to +0.018 ATR per call. Two obvious levers are already spent, and the logs say so. Raising the vote threshold buys precision and hands the expectancy back - SP500's precision climbs 38.9% -> 57.3% across the six rungs while its book expectancy FALLS +0.39 -> +0.21 - and the rung search already takes the best rung available. Exits do not rescue it either: the 64-cell sweep fails Sidak on all six, p 0.19 to 1.00. Narrowing the label to drop d=+1 was checked and rejected: it is negative on SP500/USDCAD/XTIUSD, neutral on XAUUSD/USDJPY and strongly POSITIVE on EURUSD (+0.769, n=116). What is left is discrimination at fixed coverage, and 294 inputs against ~4,700 independent observations is 16 per first-layer weight - the overfit regime, where an out-of-sample overfit IS a false pivot. The keep-screen already measured which columns carry information, and the open question blocking a prune was whether the masks agree. They do. Intersected across all 24 members - six charts x four architectures - swing[5..8] and ma[0..1] are UNANIMOUS, ma[2..4] carry 23/23/20 votes, and 16 of 49 columns are kept by nobody. This mask is every column kept by at least half the fleet: swing[5..8], ma[0..4], crossasset[3,4], volume[1,3]. Width 294 -> 78, first-layer budget 16 -> ~60. Two of the votes are worth reading twice. swing[0..4] - the five CONFIRMED-PIVOT features - scored 4, 2, 0, 2 and 1 of 24: the label is a ZigZag pivot event and the ZigZag geometry carries almost nothing, while trend position carries everything. And alt[0..11] scored 4, 1, 1, 1 with atr at zero, so a whole rate-limited external pipeline buys nothing under this label. Alt stays ENABLED and merely unmasked, so the screen keeps reporting and the finding stays falsifiable. Implementation keeps one authority for block order and width: CFeatureBuilder::FeatureBlockTable, which FeatureSlotName, the kept count and the emit-time compaction all read. The mask is stated BLOCK-RELATIVE so toggling a block cannot silently re-point it, and applied once per bar at the existing sanitize seam rather than inside nine emitting blocks. m_neuronsCount is set from the counted kept columns, never from a hand-written subtraction, and a one-shot width check fires if the two ever disagree. FEATURE_MASK_VERSION 0 restores the full set exactly, including the pre-mask fingerprint. This is a measurement, not a conclusion: the screen tests each column's MARGINAL information and cannot see a column that is useless alone and useful in combination, so judge the retrain against the per-chart false-pivot rate and book expectancy above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5a7d96c948 |
fix(gate): certify the BOOK, report the split - the per-side test was selecting
WarriorRungBothSidesProfitable required each side's mean ATR/call to be > 0.0. Per-call returns at the hold horizon have SD ~3.1 ATR - backed out of the direction policy's own printed band - so a side's standard error at a few hundred independent calls is 0.24-0.35 ATR. The test compared that against exactly zero. It did not merely fail to detect. The derived rung is chosen from six, and on three charts the short side came out substantially negative at five of them and a hair above zero at precisely the one that certified: USDCAD eS: -0.23 -0.18 -0.13 -0.13 [+0.01] -0.18 USDJPY eS: -0.27 -0.22 -0.26 -0.23 [+0.02] -0.03 XAUUSD eS: -0.44 -0.39 -0.28 [+0.02] -0.08 -0.68 A short side does not pay at 25% and lose 0.13 to 0.23 either side of it. That is a best-of-six finding the cell where noise crossed zero, with no family-wise correction anywhere on this test - Sidak guards the precision test only. XAUUSD is the extreme: 337 consecutive eras refused on this clause, one era passed on eS=+0.02, it deployed 39 seconds later, and era 339 failed again. The gate is now the BOOK: pooled expectancy over both sides at the certified rung. One number, the largest sample available, and what the account is actually paid. It still catches what the clause was added for - a side losing enough to matter drags the total under by itself. AND IT IS A FLOOR, NOT A TEST. Book expectancy is no more significant than the sides were; nothing in this fleet clears 2 SE on payoff, where precision clears at 23-47. Demanding significance would deploy nothing, ever - the prove-your-edge trap this codebase has backed off twice. Refusing a measured-negative book needs no power, only a sign that is already the best estimate available. The split stays measured and printed beside it as a sizing and direction input, never a gate - same doctrine as alpha, the barrier sweep and the topology floor. Checked against the 2026-09-01 fleet: all six still deploy and every derived rung is unchanged. The higher rungs that newly clear the book test fail the coverage floor instead (XAUUSD 25% at 4.0% vs 6.7%, USDJPY 30% at 4.4% vs 6.6%, EURUSD 30% at 4.7% vs 6.7%). Report: SIDE-FAIL -> BOOK-FAIL, and the threshold sweep now prints book|L/S per rung so the selection pattern above is legible at a glance instead of needing six eras side by side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9b69ef09a0 |
fix(direction): the by-side policy was a sign test on overlapping counts
WarriorMeasuredSideDirection() decided which half of the book to switch off with `mean > 0.0` per side, gated only on n >= 100 RAW calls. No error bar, and no deflation for label overlap - in a codebase where every other verdict carries an exact binomial or a Sidak correction. What it did live, on one chart, inside fifteen minutes: 19:35:41 EURUSD long -0.038 (n=591) short +0.011 (n=559) -> SHORT ONLY 19:50:45 EURUSD long +0.063 (n=3687) short -0.046 (n=3238) -> LONG ONLY The whole book inverted on five hundredths of an ATR. And those n count overlapping labels at a 5.0-bar mean lifespan, so the honest sample was about a fifth of the one being tested. Both sides now accumulate a sum of squares, so each mean carries a spread. The bar is on the EFFECTIVE count (EffectiveSampleSize, applied in Training.mqh where the member that knows this model's overlap lives - the resolver is a free function on the parent header with no object to ask), and a side is given up only when BOTH ends hold at DIRECTION_SIDE_MIN_SIGMAS = 2 SE: the side being dropped loses beyond the band AND the side being kept pays beyond it. "Loses" is the same bound on the negated mean, one expression, so the halves cannot drift. An interval straddling zero answers false to both - the state the sign test could not express, and exactly where EURUSD was when it flipped. Two sigma, not one, per the operating-point note that a 1-SE band is not enough when the quantity is noise-dominated. The spread and the independent count are persisted as WSTC. A WSTB record restores the means for reporting with a 0.0 spread, which the resolver reads as "no interval" and resolves to BOTH SIDES until an era re-measures - the right degradation, since an older file's verdict came from a test this build no longer trusts. None of this says the current verdicts are wrong: at 20:00 SP500 was one-sided on the drift screen (+4.09% of the average bar) and XAUUSD, USDCAD, USDJPY and EURUSD on the measurement, with XTIUSD on BOTH because both its sides lose. It says four of them were ACTED ON hours before the evidence supported them, and one of them acted on noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c4fd0be7c6 |
fix(topology): the capacity re-derive tested one of three derived sizes
The one-shot re-derive in AutoTune.mqh is the unattended answer to the cold-start floor, and it works: it fired eight times on 2026-08-31 at era 0-1, moving the first layer on PAI/CONV across four charts. Timing was never the constraint - the first pool file appeared at 13:40:42 and all six had published by 13:41:46, about ninety seconds after sizing. (The "sixteen minutes" in the old comment was a republish mtime, not a first publish; corrected here.) It asked only ComputeFirstLayerWidth(). SP500's and XTIUSD's LSTM and ConvLSTM had an UNCHANGED first layer and a HALVED LSTM hidden size - 8 where the same charts carried 16 that morning - so the test read "grown but the width is unchanged", latched m_capacityResizeDone and closed the door on the one dimension that had moved. Those six models carried a training-alone topology for the rest of their lives, and they are exactly the ones yesterday's init-time guard then found and destroyed. Now it measures all three, in the dependency order the ladders read each other in (conv -> LSTM -> width; ComputeLstmHiddenSize reads ConvFilterCount, ComputeFirstLayerWidth reads both), through the members because that is the only way those reads see a candidate. They are restored immediately - ResetWeights() remains the only thing that commits a shape, and it re-measures them itself. And it counts only the stages this model has. A Perceptron has no LSTM block and no conv stage; rebuilding one because an LSTM ladder step moved is a retrain bought for nothing. The log line names whichever dimension moved instead of always saying "first layer". Topology.mqh's init-time report is now documented as what it is: the last resort, not the mechanism. Reaching it means a model outlived the era-8 window still floored, which is a bug in this block rather than a case for deleting a trained model's weights. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2f17801d6b |
fix(persistence,topology): the .stats magic sorted below WST1, and the resize guard believed it
WSTA/WSTB were typed 0x57535341/0x57535342 - third byte 0x53 ('S') where
the format needs 0x54 ('T'). They spell "WSSA"/"WSSB" and sort BELOW
WST1, so every `magic >= WST_MAGIC_n` test in LoadModelStats went false
and a WSTB file read back as a bare WST1: no tier ladder, no ensemble
record, no g_ensDeployApproved, no certified precision pair, no barrier
widths, no swept pair, no direction evidence.
Measured 2026-08-31. The 13:37 build wrote WST9 and the 15:35 restart
restored the ensemble record on all six charts; the 15:31 build wrote
WSTA-mistyped and at 16:41 not one chart could. SP500 (deployed 14:47,
54.8% vs 14.2% on 374 fired bars, 22.5 sigma) and XTIUSD (14:55, 47.2%
vs 13.9% on 897, 28.8 sigma) resumed with the deploy flag reading false
and every member mute for want of a ladder.
The mistyped files are ACCEPTED, not rejected. Only the label is wrong;
the fields behind it were written correctly and in order, and rejecting
them - a mismatch is treated as absent - destroys exactly the state this
sidecar exists to protect. They are normalised on read; writes use the
correct value. Every magic is a named constant now, because a hex
literal does not show you its own ordering and the ordering is what the
">=" chain rides on.
AND THE RESIZE GUARD ASKED THAT SAME FLAG.
|
||
|
|
636ec4c078 |
feat(direction,chart): decide the side on measured expectancy; draw the exit vote
TWO DEFECTS, BOTH VISIBLE IN THE 2026-08-31 15:35-15:41 LOGS. 1. THE DIRECTION POLICY GATED ON A PROXY WHILE THE MEASUREMENT SAT BESIDE IT. The deploy gate refuses a chart whose one side loses and its refusal text says the remedy out loud - "switching the losing side off with tradingdirection raises expectancy arithmetically ... the drift screen does that automatically when the bias is large enough". It did not fire. Measured today: XAUUSD long +0.228 short -0.522 drift +1.36% -> screen says BOTH USDCAD long +0.105 short -0.264 drift +1.91% -> screen says BOTH EURUSD long -0.050 short -0.093 drift +0.90% -> screen says BOTH Three charts blocked from certifying as one-sided books by a body-drift proxy, while the direct by-side expectancy - measured on the actual calls, at the certified rung, at the hold horizon - said the short side loses on every call. WarriorEffectiveDirection() now reads the measurement first and falls back to the drift screen only where no era has scored yet. An explicit tradingdirection still outranks both. THE MEASUREMENT IT READS IS UNFILTERED, AND THAT IS THE WHOLE DESIGN. The certified by-side numbers are accumulated BELOW the direction filter, so feeding those back into the policy would be a ratchet: block the short side once, score zero short rows from then on, and no evidence could ever re-open it. A second, unfiltered pair is accumulated ABOVE the filter and answers the question the policy actually asks - would the blocked side pay if allowed. Everything below the filter is untouched, so what the gate certifies is still exactly what the EA trades. Both sides losing resolves to BOTH, deliberately. That is a bad model, not a one-sided one; blocking a side there creates no edge and only hides half the failure from the gate. Both sides need 100 calls before either may move the policy - a thin side's mean is noise, and noise must not switch off half a book. Persisted as WSTB: a deployed chart runs no further eras, so without it a restart would drop it back to the proxy answer this replaces. 2. THE EXIT VOTE WAS INVISIBLE. Arrows are drawn in CheckOpenLong/CheckOpenShort. On a one-sided chart the blocked side returns at the direction filter before reaching that code, so with Exit_On_Reversal_Vote on, the vote that CLOSED the position drew nothing - the chart showed an entry, then a position gone, and nothing in between. Trading on a signal the chart cannot show is the certified!=traded defect one layer up. CheckClosePosition now draws an EXIT mark on its success branch, in its own SIG_EXIT_PREFIX namespace - not SIG_VOTE_PREFIX, whose sidecar and cooldown reconciliation would adopt an exit as an entry. Registered in WarriorChartPrefixes() so a purge reaches it: it lives under SIG_ARROW_PREFIX, which skipArrows protects. Colour is the ACTION, so closing a long draws in the sell colour. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2c2cdfed6a |
feat(exits): sweep the barrier grid on held-out rows; remove SL_Mode/TP_Mode
PHASE 1 of doing the trade-management search inside the EA instead of in the MT5 optimizer, for the one pair that cannot wait: the stop and the target. WHY NOT ALGLIB. The space is discrete - 8 stop widths x 8 target widths = 64 cells. At that size you do not search, you ENUMERATE. Exhaustive has no seed, no convergence question and no tuning of its own, and it answers the thing a search cannot: whether the good region is a broad plateau or one lucky cell. WHERE IT RUNS. Inside the era report's existing OOS threshold sweep - same held-out rows, same purge, same certified rung the deploy gate uses. No tester, no agents, no .set. ORDERING IS THE WHOLE POINT. MFE and MAE cannot say which barrier a trade hit FIRST, and "both were touched" is the common case, so a grid evaluated from the extremes alone would be guesswork dressed as measurement. MeasureBarPayoff() already walks the forward window bar by bar; it now records the first-touch bar offset for each grid level in each direction. One compare per level per bar on a loop that already runs. The sweep then costs 64 integer compares per fired row and reads no prices at all. Same-bar ties resolve to the STOP. Bar data cannot order two touches inside one bar, and assuming the target would be the optimistic half of an unknowable coin flip, on exactly the half that flatters the result. JUDGED AGAINST THE NULL OF THE MAXIMUM, NOT ZERO. Picking the best of 64 cells and reporting its own z is the best-of-N error this project has already made four times, including on the deploy decision. SidakFamilyP over the cells actually scored is the same correction the rung sweep and the baseline comparator use. A cell needs BARRIER_MIN_CALLS before it may win at all - a thin cell tops the grid on noise alone. The pair is STORED either way: a reader must be able to tell "swept and rejected" from "never swept", so the p travels with the pair (WSTA in .stats) and gates its use at read time, not its record. SL_Mode and TP_Mode inputs are REMOVED, and STOP_LOSS_MODE/TAKE_PROFIT_MODE with them - deleted rather than left dangling, per the RISK_REWARD_RATIO rule: a live enum with no input behind it is the shape of the 2026-07 incident where a saved .set kept feeding a deleted ordinal back in. With the inputs gone there is no ordinal left to feed, and ValidateTradeManagementInputs() loses two members. Three tiers at read time, most trusted first: the swept pair when it cleared its own family-wise test; else the mean excursions (cruder, but nothing was SELECTED to produce them, so they need no such test); else a fixed fallback that announces itself and is unreachable on a deployed chart, since a chart with no completed era cannot pass the deploy gate. Entry offset, expiration, trailing and the exit-vote flag stay inputs for the MT5 GA - they need entry-fill and path-stepping simulation this phase does not have. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1a14afb990 |
fix(topology): close the cold-start capacity floor once era 0 has written weights
The existing re-derive only fires when there are NO weights. That covers a deliberate wipe-and-restart and misses the case that has now happened twice: a cold fleet start sizes every topology against an empty pool, era 0 writes a .nnw minutes later, and from that moment the floored .cfg is authoritative forever. Measured today: six charts booted 13:40:13-16, every census reported "0 compatible row(s) from 0 peer file(s)", and the first pool publish landed at 13:56-13:58 - sixteen minutes after the decision. SP500 and XTIUSD came out with lstm 128->8 where the same charts ran 128->16 that morning on a census of 30,161 rows from 2 peers. Half the recurrent state, pinned. The discriminator needs no new persisted state. The census only ever GROWS as peers publish, so a .cfg pinning a SMALLER topology than today's measurement is a .cfg written under a poorer census. Strictly larger only - shrinking a topology under trained weights is what adopt-don't-compare exists to prevent. It fires only when there is nothing to protect: not deploy-approved, and not tier-ranked. Both come from .stats, read at the decision rather than assumed. A deployed model is trading and an already-ranked one has completed a real pass 3; either way the weights are the asset and the larger topology is a retrain, not a resize - so that case logs the comparison and keeps the weights. An unranked, undeployed model has era-0 noise behind it, which is what makes discarding it free. It cannot loop: after the rebuild the .cfg matches the census. Reuses the existing discard-and-rebuild branch verbatim - the only change is which condition reaches it. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f942cb2ef2 |
feat(exits): size the stop and target from measured excursions; gate indicators on a new bar
Two changes, both driven by the same single-backtest profile (6,607,688 ticks, 165 s, SP500 H4 2019-2026). INDICATORS, 39% OF THE PASS. m_indicators.Refresh() cost 61.4 s at 9.30 us per tick, running on every quote regardless of Expert_EveryTick. Every indicator this EA registers is computed from CLOSED bars - ATR, the MA, the ZigZag, the feature block - so their value cannot change between two ticks of the same bar and the refresh was recomputing a constant. It is now gated on a new bar, with its own CNewBar watermark: IsNewBar() consumes the transition and Refresh() runs before Processing(), so sharing m_newBar would have silently disabled the SetDirection gate. The exit invariant holds. What changes intrabar is price and position, and neither comes from an indicator buffer: m_symbol.RefreshRates() still runs on every tick and is what CheckClose/CheckTrailingStop/pending maintenance read. A trailing stop still moves on any tick; it now compares the live price against an ATR from the last closed bar, which is the ATR it should have been using. ProtectOpenPosition() keeps its ungated copy - it runs only on ticks Refresh() declined, and never appeared in the profile. SL_MEASURED / TP_MEASURED (both value 100), now the shipped defaults. The fixed pair was SL x2 / TP x6 against a measured SP500 MFE of 2.59 ATR and MAE of 2.71: the stop sat INSIDE the average adverse move and the target BEYOND the average favourable one, so the average trade was stopped out before reaching a target it does not reach. That converts winners into losers mechanically at any precision, and no model work can fix a barrier pair pointing the wrong way. These are NOT the removed SL_INTELLIGENT/TP_INTELLIGENT. Those scaled the barriers by the model's own CONFIDENCE - an over-confident model gave itself a tighter stop and a wider target, which is why they were deleted. These read a MEASUREMENT of what the market did on the bars this ensemble fired on: the mean adverse and favourable excursions in ATR at the hold horizon, already computed by the era report that certifies the deploy and previously printed and thrown away. Only the WIDTH is measured; the reward:risk that falls out is reported, never targeted - the ratio is policy, the width is what pays. A FRESH ENUM VALUE, never the vacated -1 the removed members held: MetaTrader does not validate enum inputs, so a .set saved by that build still feeds -1 in, and reusing it would silently give a stale file a new meaning. ValidateTradeManagementInputs() keeps rejecting -1 and now accepts 100. Plumbing follows the derived-threshold route exactly, because ExpertSignalCustom.mqh is the PARENT of the filter that owns g_ensBest* and cannot read them: stashed inside the isBetter block (so the widths describe the bars the CHECKPOINT fired on, never a later era's), persisted as WST9 in .stats (a deployed ensemble runs no further eras - the tier-ladder failure one layer along), and published per tick via PublishMeasuredBarriers(). The -1/-1 "not measured" state is published too, so a reset-weights cannot leave a stop sized off a dead ensemble. With no measurement it falls back to the shipped fixed presets and says so once per run. It deliberately does not substitute a plausible number: an invented width would be indistinguishable from a measured one in every log afterwards. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
04cc345661 |
perf(tester): split the per-tick profile so the slow bucket names itself
Three turns of reasoning about where a 6.3-minute pass goes have produced three hypotheses and no measurement. The profiler that would answer it already existed and printed nothing: across 129 optimization passes on 12 agents the "tester pass profile" line appeared ZERO times, while OnInit's output appeared on every one. Two fixes, both aimed at ending the guessing rather than at being right. 1. The line is now built by WarriorTesterProfileLine() and printed from OnTester() as well as OnDeinit(). OnTester runs on the agent at the end of the pass, BEFORE OnDeinit. If the line appears there and not in OnDeinit, an optimization agent is discarding OnDeinit's Print; if it appears in neither, g_tpTicks is genuinely 0 and the instrumentation never ran. Those need different fixes. The zero-tick case now prints its own explicit line instead of staying silent, because a profiler that says nothing when it fails is indistinguishable from a fast pass. 2. "Expert.OnTick" was one bucket containing both halves of the question. CExpertCustom::Refresh() runs on EVERY tick whatever Expert_EveryTick says - correctly, since an open position must be manageable on any quote - and under a 1-minute-OHLC model that body executes millions of times per pass. Its three steps are now timed separately: TCHasEnoughHistory(), RefreshRates(), and m_indicators.Refresh(). They are reported as a SUBSET of Expert.OnTick, not as siblings, because double-counting a bucket is how a profile lies. ProtectOpenPosition()'s copy of the same refresh counts into the same bucket rather than hiding on the declined-tick path. The accumulators move to System\TesterProfile.mqh: ExpertCustom.mqh has to see them and is included long before Warrior_EA.mq5's own globals. Every bracket is guarded on g_tpActive, which OnInit sets only for MQL_TESTER/OPTIMIZATION/FORWARD - a live chart never reads the clock for it. No behavioural change to any path. Compiled 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a9c21fdf10 |
feat(tester): gate input RANGES at OnTesterInit, not just input values
ValidateTradeManagementInputs() hard-gates input VALUES at OnInit because MetaTrader replays a saved .set without validating it. Nothing gated input RANGES, so an optimization would happily sweep a parameter that cannot affect a trade - or one that destroys the run - and score every pass as if it meant something. Measured on the 2026-08-31 session: Signal_ThresholdOpen was swept over 10..80, which MetaTrader expands to twelve PERCENTAGE_PRESETS members. Its own label reads "SEED (derived after era 1)": every pass restores the pinned rung from .stats into g_ensDerivedThreshold, and OnTick publishes it over m_threshold_open before the first bar closes. Twelve identical strategies, a 12x multiplier on a fifteen-hour estimate, and no warning anywhere. A wasted dimension does not only multiply runtime - it fills the GA's fitness landscape with plateaus, so crossover produces clones. 115 of 129 passes scored 0. WarriorGuardOptimizationRanges() runs once in the controlling terminal, the only place ParameterGetRange/ParameterSetRange are legal and the last moment a bad sweep is free to stop. Two classes: REFUSE - keys BuildModelFingerprint(), or the capacity budget upstream of the neuron count inside it (Use_Training_Pool). These select a different MODEL, not a different strategy: the agent resolves a .nnw filename that does not exist and either votes silently or trains inside the backtest, and the pass still lands in the results table looking real. Session stopped with INIT_PARAMETERS_INCORRECT. PIN - a seed, or inert on an inference-only pass: the training-only inputs, the DB ranking pair, presentation flags, and the news filter (calendar error 4806 over historical dates, so it fails open on every bar - the message says so, because a config optimised here trades through news live). The sweep is switched off and the operator's own value kept. tradingdirection is deliberately NOT in the table - it is a legitimate override, and WarriorEffectiveDirection() is explicit that the screen never overrules an operator who chose. A degenerate range spanning BOTH and the screened side is reported instead, from the controlling terminal where the drift screen has full history. An unresolved name warns rather than refuses: the table addresses inputs by string, so a rename unbinds it, but a false refusal would make the EA impossible to optimize at all. The summary line prints on every optimization, clean ones included, so a table that has come unstuck shows up on the first run - same doctrine as the drift screen reporting on charts it does not restrict. Compiled 0 errors, 0 warnings. Live paths untouched: nothing outside OnTesterInit() is reachable from this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
818ffc7d8c |
docs(tester): the DLL guard never prevented the load - name the real restart blocker
Two comments claimed CNet::InitComputeDll()'s tester guard "avoids agent-side file-lock/synchronization failures" and that an inference-only backtest "never loads WarriorDML/WarriorCPU.dll". Both are false, and the second one is contradicted three lines below its own sibling comment, which correctly notes that MetaTrader rejects a missing DLL "at program load before any of this code can run" - i.e. the #import is resolved before MQL5 runs, so the guard can only stop CALLS, never the load. Measured 2026-08-31: all twelve metatester64.exe agents had their own sandbox copy of WarriorCPU.dll mapped and locked. Recompiling the .ex5 makes the terminal re-push the program's dependency set to every agent; the DLL write fails with Windows error 32 (ERROR_SHARING_VIOLATION) and the tester aborts the session after three retries ten seconds apart. It presents as "the backtest will not start", with a clean EA teardown and nothing in the tester journal. No code change: MQL5 cannot unload an imported DLL. The comments now name the failure, its log signature, and the operational remedy (kill the resident agents after a recompile), so the next diagnosis does not start at OnDeinit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b512b3aa18 |
perf(signal): gate the VOTE on a new bar, keep the VETOES per tick; cache the news filter
Operator: "nothing should run every tick if expert_everytick is false." Almost right, and the exception is why this is a split rather than one guard. VOTE filters (GetPatternCount() > 0 - the AI members) read dPrevSignal, which RefreshConvergedSignal() only refreshes when a bar closes. Re-running them per tick recomputes the same number from the same inputs. Now gated on CNewBar, with their contribution carried in the member scratch array across the bar's ticks. Journaling and the raw draw are skipped with the call, deliberately: both read state only a real Direction() call refreshes, so running them against a cached value would re-journal one bar's pattern under many timestamps. VETO filters (GetPatternCount() <= 0 - news, session, risk guard) signal by returning EMPTY_VALUE from that same call and what they answer CHANGES WITHIN A BAR. An entry can be placed on any tick, so freezing them to bar granularity would let a trade through a news window that had already opened - and the risk budget is required to enforce at quote frequency, not per bar. They stay live. A cached veto still vetoes for the rest of the bar. Expert_EveryTick=true disables the gate entirely, which is what that input means. The abort rollback now revokes only filters actually entered this tick: RevokeVote() restores a BeginVote() snapshot, and revoking one that never ran would roll it back to a stale one. An aborted pass also drops the cache. THE NEWS FILTER WAS THE EXPENSIVE VETO, and it had no cache at all: one CalendarValueHistory() per relevant country on every call. Now cached to the minute, which is EXACT rather than an approximation - the window is [now-N, now+N] and calendar events carry minute resolution, so the set inside it cannot change until TimeCurrent() crosses a minute. Country codes resolved once. And its failure Print was UNCONDITIONAL. Error 4806 (no calendar data for the window) is the normal case in a backtest, so that was a journal write per tick per country - journal I/O being the most expensive thing in a tester pass, the diagnostic cost more than the filter it was diagnosing. Once per run now, and it says plainly that the filter is INERT for the run, so a config optimised there is one that trades through news. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
75406a5b04 |
perf(signal): stop building throwaway strings on every tick in Direction()
Direction() runs on EVERY TICK - the stdlib's m_every_tick only moves StartIndex(), it does NOT gate CExpert::Processing(), which opens with m_signal.SetDirection(). And Direction() RECURSES into every filter, so on this fleet that is ~8 invocations per tick. PrintVerbose() is a plain function, so its argument is constructed whether or not VerboseMode is on. Two of the calls in that path are unconditional - a concatenation plus IntegerToString, and another plus DoubleToString - so a tester pass over years of history built and discarded millions of strings to produce no output. Both are now behind if(VerboseMode); the rare error branches keep the plain call, where a bare literal is not worth guarding. The filter loop's `directions[]` was a local dynamic array, heap-allocated and freed on every one of those calls. Now a member sized once, after which ArrayResize() returns immediately. Behaviour is unchanged in both cases - this is call-count work, not logic. NOT DONE, and the earlier suggestion to do it was WRONG: caching Direction() per bar is not behaviourally neutral. The veto filters (news, session, risk guard) signal by returning EMPTY_VALUE from the same call, and they legitimately change WITHIN a bar - a news window opening, a session closing, the risk budget breaching. Freezing the vote to bar granularity would freeze those with it, and the risk budget is required to act at quote frequency, not per bar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
634f8459b2 |
fix(train): the prebuild had five ways to refuse and told you none of them
SP500 sat wedged for 46 hours printing "Train() is taking the 'arming the first
label-cache prebuild' branch" every minute, 2810 times per member, and the log
could not distinguish an unsynchronised series from a cold ZigZag handle from a
still-settling history depth. StartLabelCachePrebuild() has five early returns
and exactly one of them printed.
m_prebuildBlockReason is now set at every return and printed by
ReportTrainStall() as its own line, with what to do about each: an
unsynchronised series names the bar count and says only the terminal can clear
it; a cold ZigZag distinguishes "fills in seconds" from "failed Create(), needs
a re-attach"; SettledBars names the number it returned against the depth it was
asked for. Cleared on the arming path so a stale reason cannot outlive the
block it described - a stale explanation sends the reader after a problem that
is already fixed.
Same defect as
|
||
|
|
752fae9e02 |
fix(signal): keep the cooldown behind order validation - the arrow moves, the clock does not
Operator decision: reverse the reversal. a60cfbc stands - VoteCooldownCommit()
fires only once OpenLongParams/OpenShortParams validate, so a broker rejection
never burns the spacing budget a real trade needs. Those failures are transient
(ATR warm-up, stops-level), not a property of the vote, and paying a full
cooldown for one costs a tradeable signal.
The arrow stays where
|
||
|
|
32afb003f1 |
fix(chart): an arrow is a statement about the VOTE, not about trade management
The live filtered view drew the arrow AFTER OpenLongParams/OpenShortParams validated and ERASED it when they failed - invalid SL/TP, stops-level, ATR warm-up, unsynced swing history. So the same vote history drew a DIFFERENT arrow set under a different SL_Mode, which makes the chart a picture of the trade policy instead of the signal. It also made the two layers disagree by construction: the overlay reconstruction has always drawn on vote + threshold + direction alone, with no OpenParams anywhere. Live arrows and replayed arrows were answering different questions in the same namespace. Trade-management effects are measured in a real backtest and in the tester GA - never on this chart and never in the era report, which already states it pays "no stop and no target - the SIGNAL, not an exit policy, and it GATES NOTHING". Training was already clean: zero trade-management references in Training.mqh, Labels.mqh or Labeling/. The signal is now declared at one point - vote cleared, direction allows, declustering kept it - and the arrow and the cooldown commit both happen there. They moved together deliberately: an arrow with no cooldown behind it draws a second arrow on the next bar and reintroduces the clustering CSignalDeclusterPolicy exists to prevent. ONE BEHAVIOURAL CONSEQUENCE, stated rather than buried: an order-shaping failure now CONSUMES the cooldown, so the retry RevokeVote allows is spaced like any other signal instead of being re-offered on the very next bar. SL/TP dropped from the arrow tooltip and from DrawVoteArrow's signature for the same reason; it reads "SIGNAL BUY @ price | vote N >= M". EraseVoteArrow() removed - its only caller was the failure branch. The overlay sweep still deletes an arrow on its own verdict, which is a statement about the vote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
29cab08765 |
fix(gate): the refusal blamed PRECISION for a by-side failure - with numbers that PASS
tradeableOK = voteGate.tradeable && bothSidesPay, but the stage-3 refusal only knew how to attribute the voteGate half. Its chain is coverage -> one-sidedness -> PRECISION, where PRECISION is the FALL-THROUGH. So a best era that failed only because a side loses money landed in the fall-through and printed: SP500: "THE BEST ERA FAILED ON: PRECISION - 51.1% against a 21.8% bar" 51.1% clears 21.8% by 29pp. The real failure was a short book losing 1.00 ATR on every call it made. Operator caught it by noticing SP500 has HIGHER precision than XTIUSD (48.4%) and still would not deploy. Same defect class as the finding that motivated the by-side rule in the first place: the quantity was computed, printed elsewhere, and not consumed by the thing that reports the decision. Adding a condition to a gate is not finished until the refusal can name it. Now stashes the best era's by-side expectancy alongside the coverage/precision numbers it already kept, and names the losing side explicitly - including the LONG and SHORT case. Placed BEFORE the precision fallback, because a fall-through must never be reachable by a condition that has its own name. Also reverts TrainingOptimizer / OOSSplit / VolumeData / NewsFeatureWindowMinutes to inputs (operator decision): they are "train a different model" choices and stay operator-facing. The comment now says what actually matters about them - never put one in a tester GA sweep - rather than hiding them. Indicator periods were never the issue: the AI MA feature already reads the auto-tuned value via CFeatureBuilder::InitMA -> m_indicatorTuner.maPeriod, never the raw seed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
371eb184fb |
feat(direction): act on the drift instead of only printing it; retrain-forcing knobs are no longer inputs
THE DRIFT SCREEN. The deploy gate already refuses a chart whose one side loses on every call, and its refusal names the remedy: "switching it off with tradingdirection raises expectancy arithmetically". Nothing acted on it - tradingdirection is hand-set and defaults to BOTH, so the drift was measured, printed, and ignored. Expert/Signal/DriftScreen.mqh walks every bar's own open and close: meanNet = mean(close-open), meanAbs = mean(|close-open|), ratio = net/abs "the average bar's net direction as a share of the average bar's size" - scale-free, so one constant serves the fleet. No fitted term, no cost term, no significance test: at 15-50k bars everything is significant, and the question is whether the drift is BIG ENOUGH to give up a side, not whether it is real. Measured on H4 across both archived feeds per instrument: SP500 +4.04% / +3.65% up 53.8% / 52.5% of bars <- the only one over 3.5% FTSE +4.20% / +2.58% USDJPY +2.94% / +1.04% USDCAD +2.24% / +0.22% XAUUSD +1.88% / +2.61% WTI -0.74% / -1.14% (the only negative) The two feeds for one instrument disagree by up to 2pp, so the MAGNITUDE is feed-dependent and only the sign replicated - hence one shared threshold set where only a large, both-feed-agreeing drift crosses, not a per-symbol table. At 3% exactly one fleet symbol crosses: SP500. That matches the gate's own by-side expectancy at the deployed rung (SP500 short -1.00 ATR/call, while XTIUSD short pays +0.13 and so stays two-sided despite a mild negative body drift). Two independent measurements, same verdict. It resolves through WarriorEffectiveDirection() - the existing single resolution point - so the sweep, chance rate, deploy gate, live path and overlay all pick it up and the gate re-certifies the one-sided book for free. An explicit tradingdirection always wins; the screen can only narrow BOTH. Printed on every chart including the ones it does not restrict, because a screen that only speaks when it fires cannot be calibrated. THE BLOCKED SIDE STILL EXITS. Verified, not assumed: the direction filter sits only in CheckOpenLong/CheckOpenShort, while CheckClosePosition reads the raw signed m_direction. So a Sell vote on a long-only chart cannot open a short but does close the long, once Exit_On_Reversal_Vote is on. RETRAIN-FORCING VALUES ARE NOT INPUTS. TrainingOptimizer, OOSSplit, VolumeData and NewsFeatureWindowMinutes each key a fingerprint and none is a feature enable flag. A GA sweeping any of them re-keys every .nnw, restarts all four members from era 0 and destroys the deployment it was tuning - silently, since a new key is indistinguishable from a first run. All four verified at their defaults on the six live charts first, so the fingerprints are byte-identical and no retrain is forced. Enable* flags stay inputs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eed262c416 |
fix(chart): the vote-arrow file stamped the threshold from INIT, not the one in force
An arrow is a claim about a threshold, so the store discards its cache when that threshold changes between sessions. Correct rule, stale input: the stamp was captured once in Configure() during OnInit and never refreshed, while the derived rung goes on MOVING for the rest of the session (it follows each era's rung until a checkpoint pins it). So the two files written in the same OnDeinit disagreed by construction: .stats saved the CURRENT threshold, .votearrows saved the init-time one. Next start compared them and threw away a perfectly good arrow set. Measured 2026-08-27 22:26 - four of six charts lost everything: XAUUSD stored 25 / now 20 XTIUSD stored 25 / now 5 SP500 stored 25 / now 5 USDJPY stored 15 / now 20 The two that survived, EURUSD (173 arrows) and USDCAD (13), were the two whose rung happened not to move. WarriorVoteArrowThreshold() is now the one expression, read live at the load comparison AND at every save; the cached member is gone, so it cannot go stale again. Only the close threshold is still passed to Configure(), because VOTE_EXIT_DISABLED_THRESHOLD genuinely cannot move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9eda06d2c5 |
fix(gate): expectancy is the bar - drop the alpha test and the cost term
Two operator corrections, both reversing decisions I made this session.
1. THE BAR IS POSITIVE EXPECTANCY, NOT PROVEN EDGE.
|
||
|
|
f999139156 |
fix(gate): judge each side on ALPHA, not raw payoff - the raw test had it backwards
Correcting what I shipped in
|
||
|
|
5304cfb876 |
refactor: delete the machinery I added that was not earning its place
Self-audit against YAGNI. Three things from this session's commits were
configuration nobody asked for, and one was a measurement report pasted into
source.
DELETED - SignalCooldownScopeOverride + WarriorSignalCooldownScope(). I added a
source override for the cooldown scope, then concluded in the very next commit
that source-overrides-operator is the wrong pattern and stood it down to -1.
What was left was a mechanism whose only state is "disabled" - the definition of
speculative generality. The scope is read straight from its input now. The thing
that actually solves "which value is running" is the init line that PRINTS the
resolved value, not a second place to set it.
DELETED - REQUIRE_BOTH_SIDES_PAY. A #define that was always true, added so the
new gate could be "reverted". Nobody asked for that switch and a gate condition
that is optional is not a gate condition. The rule is either right or it is not;
if it turns out wrong, git is the revert mechanism.
TRIMMED - 38 lines of comment in the ensemble verdict listing all six charts'
by-side payoff numbers. Those are a MEASUREMENT: they belong in the commit that
made the change and in the session record, not pinned in source where they go
stale on the next run and start actively misinforming. Two lines left saying
what the code does and when it can be false.
TRIMMED - the cooldown override comment from 11 lines to 4. Same reasoning.
KEPT, with the case for each:
CSignalDeclusterPolicy replaced FOUR copies of one rule that had silently
diverged into two different units and two different
windows. Net negative lines.
PivotLabelFillTarget replaced FIVE hand-rolled target vectors and is the
only place the sum-to-1.0 gradient invariant lives.
PivotLabel* window fns the -1 sentinel collision they fix was a live defect.
CRunningMean two meanings were sharing one accumulator, which is
how the purge came to be sized off the wrong one.
LABEL_RESOLUTION_CAP_BARS a bound on a MEASURED quantity feeding the training
split. The floor is load-bearing; the cap stops a
degenerate ZigZag eating the training set.
Test_PivotLabelWindow 161 of the 195 net added lines. Two of the three
things it pins were live bugs this session.
Net across the whole session, production code only, excluding tests:
+479 / -390 = +89 lines, and in exchange: decluster rule 4 copies -> 1,
target builders 5 -> 1, chart reconciliation 2 -> 1, twelve loose NMS members
-> two policy objects.
Compiles clean, 0 errors 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b12c2c0ac0 |
fix(gate): pick the rung that PAYS on both sides, not the one with the best precision
Completes the previous commit. Gating the final verdict on by-side payoff was right but incomplete: it refused the era instead of choosing a rung that works. THE RUNG SELECTION WAS SELECTING FOR ONE-SIDEDNESS. It walked down from the top and took the highest rung clearing the RATE gate. Precision rises with the rung; drift-free payoff falls. Both are the same mechanism - a higher rung means more member agreement, and on a trending instrument more agreement means more agreement WITH THE DRIFT. Visible in the counts: SP500 fires short n=0 at its 15% rung, XTIUSD is 415 long / 94 short. So the rule "take the highest rung that clears precision" was, on this fleet, "take the rung where the short book has been squeezed out". The per-rung by-side payoff was already computed (sideHlong/sideHshort, printed as the CONVICTION CURVE) and the selection loop never looked at it. It does now: walk down, stop at the first rung that clears the rate gate AND pays on both sides. Replayed against the measured curves from this session: EURUSD 15% -> 10% both sides pay (+0.157 / +0.075) XTIUSD 10% -> 5% both sides pay (+0.161 / +0.049) USDCAD 15% -> -- no rung pays both sides -> REFUSED XAUUSD 10% -> -- no rung pays both sides -> REFUSED SP500 10% -> -- no rung pays both sides -> REFUSED Two charts certified at an honest operating point instead of five certified at a dishonest one. That is a better outcome than the blanket refusal the previous commit alone would have produced, and it is the same information - the earlier version just could not act on it. WarriorRungBothSidesPay() is the ONE rule, read by the selection loop, the final verdict and the sweep report. A floor at zero, not a significance test: no per-side variance is tracked here, so "positive" is the honest claim. A side that never fired at a rung is not judged - but that cannot certify a long-only book, because twoSided (firedLong > 0 && firedShort > 0) already bars such a rung inside the rate gate. SP500's 15% rung is exactly that case and is rejected before the payoff test is reached. FALLBACK CHANGED so the refusal is legible: when no rung pays both sides the era is reported at the rung the RATE gate alone would have taken, so the message reads "here is the rung you would have traded and here is what its short book did" rather than naming a rung the operator never saw. The threshold sweep line now prints L/S per rung with a SIDE-FAIL marker, so [DERIVED] moving down between eras is self-explanatory instead of arbitrary. Compiles clean, 0 errors 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eac8912dd0 |
fix(gate,cooldown): both sides must PAY, not just fire; stand down the source overrides
Three fixes off the 2026-08-27 session logs (six H4 charts, fresh from era 0).
1. THE DEPLOY GATE NEVER READ ITS OWN DRIFT-FREE TEST.
twoSided was `firedLong > 0 && firedShort > 0` - an anti-degeneracy check.
The by-side PAYOFF prints three lines below it under the heading "THE TEST
THAT IS DRIFT-FREE", and nothing consumed it. Measured at each chart's own
derived rung, hold horizon, ATR per call:
EURUSD long +0.073 short +0.115 <- the only one where both pay
USDJPY long +0.358 short -0.136
USDCAD long +0.177 short -0.070
XAUUSD long +0.076 short -0.379
XTIUSD long +0.222 short -0.065
SP500 long +0.657 short -0.273
Five of six were stamped DEPLOYABLE on 30-37% precision (chance 13-14%)
while their short book lost money on every call. XAUUSD's whole vote earned
-0.157 against its own always-long book at +0.436 and still passed.
Precision is measured against the LABEL: it says a turn was called, not that
the leg after it paid. On a trending instrument the down-legs are shorter, so
a symmetric caller with real skill still bleeds on one side - and the higher
rungs make it worse, because more member agreement means more agreement WITH
the drift (SP500 at the 15% rung fires short n=0).
tradeableOK now ANDs in bothSidesPay. A floor at zero, not a significance
test - no per-side variance is tracked, so "positive" is the honest claim.
A side that never fired is not judged; an era where neither side has a
forward window is not judged either. REQUIRE_BOTH_SIDES_PAY restores the old
behaviour. The verdict line now NAMES this when it withholds DEPLOYABLE,
because "not good enough yet" and "one side of the book loses money" are
different problems with different fixes.
Timing is deliberate: ENSEMBLE_CHECKPOINT_MIN_ERA is 20 and the fastest chart
is at era 17, so nothing has deployed yet and this costs nothing to land now.
2. THE SOURCE OVERRIDE WAS DISCARDING THE OPERATOR'S COOLDOWN.
SignalCooldownOverrideBars was 30. The charts are configured for 10. The
session log says "signal cooldown (30 bars)" 43 times across all six. That is
the same failure the override exists to fix, pointed the other way - source
defeating the operator instead of a stale profile defeating source - and it
is WORSE, because a stale profile is at least visible in the chart's own
inputs dialog and this is not. Stood down to 0, which is the exit condition
its own comment always described.
The scope override (added earlier this session) is stood down to -1 for
exactly the same reason rather than kept out of convenience.
The real fix for "which value is running" is not a second place to set it:
ConfigureAISignal now prints the RESOLVED window and scope once per chart
alongside the chart input and both overrides, and says so explicitly when
they disagree. One line kills the whole class.
3. The overlay sweep's hard-coded OVERLAY_NMS_WINDOW of 6 is gone with the
window/decluster work replayed onto main - it now uses the resolved cooldown
like every other layer.
MEASURED, and it corrects a number I reported earlier in the session: against
the 30 bars the override was forcing, 89.8% of consecutive vote-arrow pairs sat
inside the window. Against the 10 bars actually configured, the figure is 11.7%
(1038 pairs across six .votearrows sidecars, gaps counted in real H4 bars with
weekends excluded). The earlier figure was true of the window that was running
and NOT of the one the operator set; the 10-bar number is the one to judge the
fix against. USDJPY carries most of what remains (18.4%, and all 21 fleet pairs
below 7 bars) - it is also the least-trained chart, at era 3.
NOT CHANGED, and flagged rather than fixed: the source default is still SCB_30
and its comment argues 20 is the smallest defensible value, because a trade on
this label is held 5 + the median leg = 18-19 bars. A 10-bar cooldown re-announces
inside that hold. That is the operator's call, not a bug.
Compiles clean: EA and both test suites, 0 errors 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5d3f2367d1 |
fix(label): the purge was sized off the OVERLAP SPAN, not the resolution lag
Found while closing out the window shift. Two fields carried the wrong one of
two quantities the 2026-08-26 rewrite had explicitly separated in prose:
OVERLAP SPAN how many labelled bars share one pivot event. Deflates
standard errors. Equals the window width (5).
RESOLUTION LAG how far ahead the newest bar a label READS is. Sets the
purge/embargo. The witness sits ~a ZigZag leg out: 13-14.
That rewrite's own comment says "resolution lag is a different quantity and no
longer belongs here" - and then CalibPurgeBars() went on reading the overlap
one anyway, because both lived in the same accumulator under a name
(MeanLabelLifespan, headed "mean bars-to-resolution") that fit neither.
TWO CONSEQUENCES, both measured against the shipped split (30% OOS, 15% calib
band, ~7000 IS bars, ~1000 band bars):
1. THE PURGE UNDER-COVERED BY ~9 BARS AT BOTH BOUNDARIES. The newest IS bar's
label reads ~9 bars into the calibration band; the newest calibration bar's
label reads ~9 bars into OOS. That is 0.86% of calibration observations -
immaterial to the fitted threshold, and NOT an IS->OOS leak (those are
separated by the whole band). Fixed anyway: it costs 18 IS bars out of ~7000
(0.26%), and carrying a purge that provably does not cover its own dependency
is the kind of thing that later gets cited as "the split was clean".
2. m_labelResolveAge WAS THE SAME BUG, and this one is not cosmetic. The field
is documented as "the bar its pivot pair committed on" and both consumers
read it that way, but it was assigned the lifespan:
* TrainPoolResolvedMs() is the POOL PURGE KEY - "a peer row must not carry
the future into this fit". It claimed every label was knowable 5 bars
after its entry when the truth is ~13-14, so the pool could serve a row
to another chart that had not yet reached the point where that label
existed. Cross-instrument lookahead.
* COnlineLearning's OOS payoff replay uses it as the EXIT bar, so it was
measuring at a 5-bar horizon - the one the oracle showed is worth ZERO
(perfect foresight earns ~0 at 5 bars, +1.2 ATR at 19). The corrected
value moves it toward the real hold.
LabelResolutionBars() is now MEASURED: SwingPivotDirectionLabel records
idx - witnessIdx, accumulated on every resolved bar (it is a property of the
instrument's ZigZag geometry, not of the IS/OOS split, and the purge has to be
right on the first era). Floored at the window width so it can never be tighter
than the shipped behaviour and capped at LABEL_RESOLUTION_CAP_BARS 64 so a
degenerate ZigZag cannot eat the training set. Every other consumer
(MiShiftPad, ReportFeatureHealth's sampling range, an AutoTune log) is a
diagnostic that reports the value it used; none is on the inference path.
CRunningMean extracted from CLabelOverlap, which was already exactly that plus
the ESS formula. Two meanings sharing one accumulator is what made this
invisible for a month; they are now two named instances of one type.
Also adds Tests\Test_PivotLabelWindow.mq5 pinning the window geometry, the
distance-slot mapping and the sum-to-1.0 target invariant - the two defects this
branch already fixed (the -1 sentinel collision, the five hand-rolled target
vectors) are regression cases in it. Both suites compile clean.
VERIFIED: compiles 0 errors 0 warnings; assertions independently mirrored and
all pass. NOT VERIFIED: the suite has not been EXECUTED in the terminal, and the
label's behaviour against a real ZigZag buffer is still only checked by the
first era's prebuild census line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
184de331fc |
fix(label,signal): move the pivot window into the leg; one declustering rule, measured in bars
Two independent defects, both about a rule written down more than once.
THE LABEL WINDOW SAT ENTIRELY ON THE APPROACH. It labelled d=1..5 bars BEFORE
the turn. Measured payoff by distance, fleet-pooled over 102 era rows and 3
charts at the hold horizon: d=1 +2.095, d=2 +1.743, d=3 +1.300, d=4 +0.969,
d=5 +0.769 ATR/call. Slope -0.343 per bar, monotone on all three individually.
Pure hold truncation predicts d=5 earns 14/18 = 0.78x of d=1 (~1.63); it earns
0.769, which is 0.37x - so truncation explains less than half and the rest is
the ADVERSE APPROACH, the call sitting through bars of price moving against it.
Every bar of lead the window granted was paying that.
The window now straddles the turn: d=+1..-3 (LEAD 1, LAG 3). It keeps the
best-paying approach bar and spends the rest of its width on the far side,
where direction is confirmed. WIDTH IS UNCHANGED AT 5, deliberately - it sets
the class balance, and ApplyLogitAdjustment's tau is capped at 1.2 logits with
the cap binding at EVERY imbalance, so only the uncorrected residual moves
(1.9x at width 5). Simulated over three leg-length regimes the balance shifts
by <=0.2pp, so nothing downstream of it had to move. That is also why the
target stays FLAT across the window rather than Gaussian-tapered: a taper
shrinks directional mass ~40% and tau has no budget left to absorb it.
Finality is re-derived, not weakened. One committed pivot strictly newer than
the window's newest slot freezes the whole window - the labelling pivot's
finality and the negative verdict together - because ZigZag can only ever move
its newest pivot, and only toward newer bars. Verified: the witness is the next
pivot newer than the labelling pivot in 100% of simulated cases, so minMove
still measures the leg AFTER the turn.
TGT:PVT1 -> TGT:PVT2:lead:lag. Every .nnw re-keys; the fleet retrains from era 0.
-1 IS NO LONGER A VALID SENTINEL - it is one bar into the leg. Added
PIVOT_LABEL_NO_PIVOT and PivotLabelDistanceSlot(); the by-distance payoff report
bucketed the no-pivot population and the leg bars into the same slot otherwise,
and its "directional labels only" test (`d >= 0`) would have dropped every leg
bar it now exists to measure.
THE DECLUSTERING RULE WAS WRITTEN OUT FOUR TIMES and the copies disagreed.
Rules 0-3 lived in NmsLiveAccept, PruneDirectionalClusters, pass 3's OOS tally
and the overlay sweep, each carrying a comment insisting it must match the
others. It did not:
* the two sweeps compared bar INDICES; the live gates compared wall-clock
seconds. Elapsed time is always >= bars*period because weekends and session
breaks add time without adding bars, so the live rule was strictly the most
permissive and could only ever UNDER-suppress. On H1 a 30-bar window is 30
hours against a ~50 hour FX weekend: a Friday signal never blocked a Monday
one, once a week, per chart. On session-break instruments, daily.
* the overlay sweep had rules 1 and 2 only - no cooldown, no alternation -
against a hard-coded OVERLAY_NMS_WINDOW of 6 while the live gate required
30, and its comment claimed parity with m_signalClusterWindow. It drew
reconstructed arrows 7 bars apart onto a chart whose gate requires 30.
Now one CSignalDeclusterPolicy, measured in bars via WarriorBarsBetween(), with
an unresolvable frame SUPPRESSING rather than passing. The vote layer configures
it as a pure cooldown because that is what CheckOpenPosition actually applies.
Three more that let clusters through:
* THE CLOCK WAS NEVER SEEDED. MT5 restores arrows from the .chr profile but
not the state that spaced them, so every restart, recompile or timeframe
change let the next bar fire regardless. Seeded from the newest arrow on the
chart once the progressive restore completes.
* THE COOLDOWN WAS CONSUMED BEFORE THE TRADE EXISTED. VoteCooldownAccept both
tested and committed, ahead of order-parameter validation - and the failure
branch restores the vote for retry but could not un-burn 30 bars. Split into
a pure test plus VoteCooldownCommit() at the draw.
* SCOPE HAD NO SOURCE OVERRIDE. Bars did; scope did not, so a chart whose
profile held PER_DIRECTION silently dropped rule 0 with no way to correct it.
Also DRY: five hand-rolled 3-class target vectors -> PivotLabelFillTarget(), the
one place the slot order, the smoothing constants and the sum-to-1.0 invariant
live (the gradient is target_i - softmax_i, so a vector that does not sum to 1
adds a constant drift to all three logits). Two copies of the chart-wide
reconciliation -> WarriorReconcileVoteCooldown(). Twelve loose NMS members ->
two policy objects.
Documented at CNet::backProp: sampleWeight = 0 IS NOT A SKIP. Adam still applies
decayed momentum and decoupled weight decay, t still advances, and m_batchCount++
is unconditional - so masking a class-skewed subset that way shrinks every weight
rather than ignoring the example. Drop the bar before queueing instead.
Compiles clean (0 errors, 0 warnings). Window arithmetic verified by simulation;
the label's in-situ behaviour against real ZigZag output is NOT verified here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
213b3aac15 |
fix(chart): the reconciliation never ran - it was hooked to a sweep deployed charts do not do
cooldown-recon put the chart-wide cooldown at the end of the overlay sweep. It executed ZERO times. This store's own header already said why: the sweep 're-arms only when an era ends. A DEPLOYED ensemble runs no further eras'. Five of six charts were deployed, so there were zero 'Filtered view: swept' lines in the entire session while the saved files still held 148 same-side pairs under 30 bars on XTIUSD. Moved to the completion of the progressive vote-arrow restore, which runs on every chart including deployed ones. The restore thinning alone was never going to be enough either: MT5 persists chart objects in profiles\Charts\*\chart*.chr, so arrows drawn under an older window are ALREADY on the chart when the process starts, and a freshly-thinned restore just adds to them. Two correctly-thinned sets still union into clusters. The chart is the only authority. Same construction as before: OBJ_TREND only (the line is the canonical half of a mark, matching Snapshot()), sorted by time first because object order is not time order, and the gap>0 guard so a mis-ordered set fails visibly by keeping rather than silently by deleting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4e2cdd3aff |
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own list from its own state, the overlay sweep thins its own list from its own state, and the live gate marks the current bar from a third. Each spaces ITS OWN survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart. Two independent thinning passes over one namespace produce a union, not an intersection. MEASURED from the saved .votearrows files, which is what the chart actually holds: XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0 XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1 EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0 while every producer's own log reported it had thinned correctly. The sweep's 'drew' counter says what ONE producer drew; the chart is the union. Verifying on that counter is what let this stand through four builds. The authority is now the chart itself: after the sweep, walk every SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew an arrow, this runs last. OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the same test Snapshot() uses. Sorted first, because object order is not time order and an unsorted forward walk yields negative gaps, which is how a prune once deleted 272 of 273 arrows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
0dda31534e |
fix(chart): purge stale member arrows even when there is no sidecar to restore
The purge was gated on m_arrowRestorePending, so it only ran when the .arrows sidecar had something to load. Stale arrows do not come from the sidecar - MT5 persists chart objects in profiles\Charts\*\chart*.chr independently of it. A chart carrying member arrows from a session where DrawUnfilteredSignals was ON would therefore never be cleaned, because the sidecar it would have needed is gone. Currently inert: there are zero .arrows sidecars on disk. Which is also the correction to the diagnosis in c5b9a1a's message - the '264 arrows restored' that prompted it was a MISREAD of 'queued 264 combined-vote arrows', so member arrows were never the cause of the reported clutter. The gate remains correct as defence; it was not the fix. The window was. Also silences the line when there is nothing to skip and nothing to clear: it runs on every chart on every start, and six 'removed 0' lines are noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c5b9a1ad10 |
fix(signal): a changed input default cannot reach an already-attached EA
Raising Signal_CooldownBars from 10 to 30 changed nothing. All six live charts
kept reporting a 10-bar window, because MT5 stores an input PER CHART in
profiles\Charts\*\chart*.chr and an already-attached EA ignores a changed
default entirely. This codebase already documents that trap, in the derived-
threshold comment in Training.mqh - and converting SignalClusterWindow from a
const to an input reintroduced the exact problem the const existed to avoid.
SignalCooldownOverrideBars (const, 30) now wins over the input; 0 hands control
back to the panel. Tunability per chart is kept, source-correctability is back.
Not applied when the input says OFF: an operator who switched the cooldown off
meant it, and silently re-enabling it from source would be the same surprise
pointed the other way.
ALSO gates the per-model arrow restore on DrawUnfilteredSignals. DrawObject()
returns early when the raw view is off, but AdvanceChartSignalRestore called
WarriorPlotSignalLevel DIRECTLY and never checked - so every restart repainted up
to MAX_PERSISTED_ARROWS per-model opinions per member, four members per chart, on
top of the combined-vote arrows. Same shape as the vote-arrow restore bug in
|
||
|
|
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> |
||
|
|
322c052a65 |
fix(chart): the persisted vote-arrow restore put back every arrow the cooldown removed
Clutter remained after cooldown-v3 because there is a FOURTH producer of
SIG_VOTE_PREFIX arrows: CVoteArrowStore, which replays a .votearrows file and
draws into the SAME object names as the overlay. Replaying a file written before
the cooldown existed therefore resurrects exactly the arrows the prune deleted.
On a DEPLOYED chart that is the entire arrow set. The store's own header says
why: the overlay re-sweep "re-arms only when an era ends. A DEPLOYED ensemble
runs no further eras" - which is the reason this store exists at all, and also
the reason nothing would ever have removed those arrows again.
The restore now thins to the cooldown at Load(), before the progressive draw is
armed, so it is idempotent: a file already written from a cooled chart passes
through untouched, an older one is corrected once.
IT SORTS BY TIME FIRST, AND THAT IS NOT OPTIONAL. Snapshot() walks
ObjectsTotal(), so the record is in OBJECT order - its own comment says so, and
the existing MAX_KEPT trim already sorts a copy for exactly this reason. Applying
a spacing rule to an unsorted record yields negative gaps, and a negative gap is
inside any window: that is the bug that wiped 272 of 273 arrows in
|
||
|
|
2ca32e933f |
fix(signal): the overlay cooldown prune ran backwards and left ONE arrow per chart
cooldown-v2 suppressed 272 of 273 on SP500, 320 of 321 on EURUSD, 329 of 330 on USDCAD - one surviving arrow on every chart in the fleet. The record is OLDEST-FIRST. The prune walked it backwards, so every gap came out NEGATIVE, and a negative gap is always <= the window: everything after the first arrow was suppressed. The direction was taken from the member comment on m_overlayIndex, which reads "walking newest -> oldest" and is WRONG. The sweep DECREMENTS a SERIES index (0 = newest) from MathMin(span, barsAvail-150) down to m_overlayStopIndex, so it walks OLDEST -> NEWEST. The pre-existing overlay NMS at the draw site agrees - it tests (m_overlayNmsKeptIdx - idx) and expects that to be positive for later bars. A stale comment counts as a guess, and this one cost a build. Comment corrected at the declaration so the next reader is not misled the same way. Guard added: the gap must be > 0 as well as <= the window. A non-positive gap means the record is not in the order this loop assumes, and suppressing the whole chart is precisely what that looks like from the outside - so it now fails visibly by KEEPING rather than silently by deleting. Found only because the verification was the drawn arrow count rather than an assertion that the code was correct. Compiling clean said nothing about it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
110080dc86 |
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal. That is not what the charts show and not what trades. The combined vote in CExpertSignalCustom had NO spacing rule at all - grep found not one reference to the cluster window in that file - so four individually-declustered members were averaged into a vote that could fire on consecutive bars. Measured live: 2,970 voting bars becoming 299-328 vote arrows. Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and XAUUSD 212 arrows, unchanged from before the change. The member-level rule could not touch them. Gated where the vote becomes a trade - CheckOpenPosition, beside the open-prohibition and open-market-closed checks, tracing as "open-cooldown". That is the filter chain the request asked for from the start and it is where this should have gone first. Suppression there means no order AND no live arrow, honouring the same "no arrow, no vote, no position" contract the member rule already had. THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the drawn set would contradict the traded set - the exact defect the renderer's own comments warn about. The sweep now records what it drew and prunes it backwards over that record, which is forward in time. Direction() is a TRANSACTION that can run more than once on a bar, so the live accept is cached per bar time. Without that a second call flips the bar's verdict after it has already journaled one. One resolver, WarriorSignalCooldownBars(), now serves both layers so they can never disagree about the window. 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>
|