forked from animatedread/Warrior_EA
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
026078d635 |
fix(reset): a fresh model quoted the deleted model's timeout share
Audit of Delete && Reset Weights before pushing. The button is sound - the
21:11:41 wipe deleted all six files for all four members (0 FAILED, four
distinct paths), cleared the arrow sidecars, rebuilt fresh topologies, and
re-derived the barrier geometry from scratch (2.00/6.00 fallback -> 1.21/2.43
from 10610 excursions), so the 2026-08-19 geometry-laundering fix holds. The
per-run block in Train() clears the plateau, best-score and deploy-gate state,
confirmed on the log either side of the reset (PAI 29.2->24.4, HYB 30.4->25.3,
stage 1/3 -> 0/3).
One thing it could not clear, introduced by
|
||
|
|
f8ac10c808 |
fix(replay): the exit replay held trades through the Friday flat that the label and the live EA both close
The EXIT-POLICY REPLAY line reported an expectancy from SimulateTradeOutcome beside a win rate read out of the label cache, and called them "the SAME calls". Same calls, two different walks - and the walks did not agree. TripleBarrierLabel stops at NextScheduledCloseAll ( |
||
|
|
20b7d9967e |
chore(test-run): defaults for the horizon-break-even measurement, and give the replay line a cadence
Two diagnostics on, neither in the fingerprint, so no model is re-keyed and the run resumes: VerboseMode false -> true per-era journal instead of every 25th Run_Alglib_Baselines false -> true verifies |
||
|
|
4070c5c9bd |
feat(breakeven): the break-even every layer scores against prices a trade that always resolves
CostAdjustedBreakEvenPct is risk/(risk+reward) and has no horizon term. It is the win rate a trade
needs when it is CERTAIN to end at one barrier or the other. SimulateTradeOutcome has an explicit
branch for the case where it does not - runs out of horizon, closes at the last bar seen for
whatever P&L that is - so on this label geometry the figure describes a different trade than the
one being replayed.
The gap is measurable and large. Across 21 exit replays today on SP500 H4 the geometric figure read
34.5% while the EA's own R simulation crossed zero between 27.4% (lowest positive) and 28.9%
(highest non-positive). Independent corroboration: the zero-skill reference, computed empirically
over every scored bar as max(winLong,winShort)/bars, reads 25.4% - add cost and it lands on the
same ~28%. The geometric number is the outlier, and every edge printed against it was ~6.5pp too
pessimistic: LSTM's 30.6%-win era reported -4.0pp while its replay returned +0.075 R on the same
trades.
With a timeout share t paying a mean m R apiece, expectancy is w(1+RR) + t(1+m) - 1, so
w* = (1 - t(1+m)) / (1 + RR) = CostAdjustedBreakEvenPct x (1 - t(1+m))
which needs no new geometry - the existing figure already carries 1/(1+RR).
This commit MEASURES ONLY. The replay now separates timeout exits from barrier exits and latches
t and m for the next era to read (the accumulators are zeroed at era start and filled at era end,
so a mid-era reader sees zero trades and would fall back forever). Both break-evens print side by
side on the replay line with t and m beside them, and the threshold line's REPORTED edge - which
selects nothing - switches to the horizon-aware figure so the operator stops reading a wrong sign.
DELIBERATELY NOT CHANGED: LiveMetaGate's veto and the rung selector's BarrierMinReachPct still read
the geometric value. Both are decisions - the second re-derives geometry and therefore relabels -
and t and m have so far only been inferred from a zero-crossing, never seen on a log. One era of
this instrumentation settles that.
The file already contained the argument, one branch away, in the vote-exit comment: a vote exit
produces a CONTINUOUS payoff, not a win or a loss, and that is why an exit-aware gate cannot go on
scoring win-rate against a fixed break-even. A horizon timeout is the same thing, and unlike vote
exits it is on by default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3d81fed5e5 |
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight
Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a2a207484a |
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal request only queues. Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV - three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so the damage was the freeze and the dirty chart, not corrupted state. Three changes: - BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term. Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says when the cap bit. - BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at each phase boundary. A stop and a spent budget need the same answer and only one was asked. - The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP time times the fold count. A boundary check cannot help once a phase is in flight, which is exactly the phase that was in flight when the process died. Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400 weights, and today's run duly printed a training class error of 0.000. That is knowable from the shape before fitting, so it is stated rather than left to a cross-validation that may not run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2de62241a5 |
fix(init): the OnInit failure the operator reads did not name the reason it failed
Two same-config charts collide on the config lock. AcquireConfigLock prints a precise REFUSED line, but CExpert::InitIndicators then returns a bare false, RetryInitStep retries it five times - a lock held by a live chart gives the same answer every time - and the last line on screen is "Failed to initialize Indicators after retries", which names neither the lock nor the owner. The explanation is six lines further up, under identical-looking retry noise. A refusal that cannot change on a retry now says so and stops: AcquireConfigLock records the reason, RetryInitStep repeats it and returns immediately. Found because a second SP500 H4 chart was started to run Run_Alglib_Baselines. That input is a diagnostic and is deliberately not in the fingerprint, so both charts resolved to the same filename - the guard was correct and the message was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9332a33e15 |
fix(telemetry): FEATURE HEALTH said "f30", which is a puzzle rather than an answer
The report has flagged f30 as mostly-zero (78%) on every member of every run
for days. Establishing what f30 actually IS took reconstructing the emission
order across three files, and I got it wrong on the first attempt - guessed
RSI, then MACD, both wrong because those feature blocks ship disabled.
It is spread[1], the spread CHANGE ratio, and 78% exact zeros is exactly what
that should read: the broker quotes the same spread on consecutive bars most
of the time, so the change is exactly 0. Benign, and it cost two wrong
answers to say so.
The report now names the block - "spread[1] (78%)" instead of "f30 (78%)".
The walk lists every block in the order BufferTempDataCompute emits them with
the widths Topology.mqh's m_neuronsCount sum declares, which makes this a
third place that has to stay in step with those two. So it does not stay in
step silently: the widths must total m_neuronsCount, and when they do not the
layout has drifted and every name past the drift point is wrong - so it
returns "f<slot>?" and names nothing rather than naming confidently and
incorrectly. A wrong name is worse than a bare index.
This is the same lesson as
|
||
|
|
2c19cf8408 |
fix(calibration): the field reported the raw argmax, which is not what trades
The CALIBRATION field added in
|
||
|
|
68dce35295 |
fix(imbalance): the class correction skipped the abstain class on a premise era 1 just falsified
Neutral has been excluded from the logit adjustment since 2026-08-16 - offset pinned to zero. That fix was right for its moment and it rested on two justifications, one of which is now measured false. The sound one: Neutral was then the RAREST class (10.61% on SP500 H4), so including it SUBSIDISED abstention by 1.20 logits, and with no directional edge to overcome that the model took the free lunch - OOS recall Buy:1% Sell:0% Neutral:100%. The anti-collapse mechanism was the collapse. The other: "abstention is already owned by m_dirConfThreshold". Era 1 of 2026-08-21 measured that directly, now that the operating point is fitted on calibration and reports what it reaches. Three of four members drove the threshold to 0.00 - no abstention filter at all - and STILL called a direction on only 12.9-14.5% of bars against a 37.5% label rate. At 0.00 the threshold owns nothing; the abstention is coming from the head's own argmax. So nothing was correcting the Neutral rate, and the new CALIBRATION field shows the result: all four members over-call Neutral 1.3-1.8x while under-calling Sell 0.0x-0.5x. CONV calls Sell on 1% of bars against a true rate of 25% - a flat refusal to trade one whole side. The sign has also flipped since that failure. Neutral is the DOMINANT class now (62.5%), so including it PENALISES abstention rather than paying for it. Rather than depend on that staying true, the invariant the old comment STATED is now implemented literally instead of by proxy: Neutral's offset is clamped at >= 0. It can be penalised when over-represented and can never be boosted when rare. Pinning it to zero blocked both directions; this blocks only the half that was ever harmful, and makes a return to the 2026-08-16 regime structurally safe rather than newly dangerous. The cap is now sized on the full three-class spread so it bounds the real offset range, and the log line says which way abstention is being pushed - that being the question this correction has now got wrong in both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0a046db530 |
feat(calibration): fit the operating point on the label rate instead of on edge
The margin threshold now sits where the model calls a direction as often as a
direction actually occurs. Nothing else.
WHY THE OLD OBJECTIVE HAD TO GO. It maximised `coverage x (precision -
breakEven)`, and this function's own comments were already the case against
it: over 98 consecutive fits of the shipped SP500 H4 model, correlation
between the chosen threshold and the win rate at it was -0.056, while the
era-to-era spread of that win rate (1.32pp) matched its own binomial SE
(1.25pp) to within 0.07pp. The margin does not rank trades. So the argmax
returned whichever of ~37 bins drew the luckiest sample, and the threshold
teleported 0.42 -> 0.04 -> 0.74 in three eras.
The response at the time was to build a null-of-the-maximum gate, an
effective-sample SE and a parsimony fallback to hold the noise down. All of
that is gone now, because fitting on calibration removes the problem instead
of bounding it: coverage is a ratio against a fixed denominator so it is well
determined at every bin, the target is a measured label rate rather than an
outcome, and nothing is maximised over a noisy curve so there is no best-of-N
to correct for. Net 174 lines out, 62 in.
It deliberately does not chase edge. It cannot - at ~0 measured edge no
operating point has more of it, and pretending otherwise is what produced a
threshold of 0.96 that still passed 60% of bars while the model called a
direction ~10x too often. The edge at the chosen point is still REPORTED,
just no longer what chooses it.
THREE READINGS OF ONE QUANTITY, AND THEY DISAGREE. "How often does a direction
occur" is measured in three places and gives ~7% (the scan's own tally), ~41%
(the era loop's counters, via this function's old coverage floor) and ~50%
(the ensemble gate's OOS base rate). They cannot all be right. Rather than
pick one silently, ScanDirectionalRatePct() and EraDirectionalRatePct() are
now named accessors, the fitter targets the SCAN - that is the tally the
operator reads, and the one "predict the labels as measured during the scan
phase" names - and the threshold line PRINTS BOTH every time it moves, so the
disagreement is on the record instead of buried in a derived floor.
The ensemble gate's own floor is deliberately NOT changed in this commit. If
the scan is right, a calibrated member covering ~7% of bars cannot clear a
12.4% floor and every model would fail the gate by construction; if the gate
is right, the scan tally is wrong. The CALIBRATION field added in
|
||
|
|
667f2bcb6b |
revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts |
||
|
|
68ef19797b |
fix(telemetry): one ensemble member had never printed a single era line, in any run on record
The era-progress rate limit was a function-scope `static`:
static uint lastProgressLogTick = 0;
shouldLogProgress = (nowTick - lastProgressLogTick >= 5000);
In MQL5 that is ONE variable for the whole build, not one per object. A 5s
limit meant to keep a single model's console readable was therefore a limit
across the WHOLE ENSEMBLE, and it did not distribute fairly - it starved
whichever member finishes last, every era, deterministically.
Measured on today's run: the era barrier releases the members together and
LSTM landed 2.06s, 2.43s and 2.28s behind ConvLSTM on eras 1-3, against a 5s
window it could never reach. LSTM printed zero era lines. PAI, CONV and HYB
printed all of theirs - 3 each this run, 17 each in the 10:00 run, LSTM 0 in
both, and 0 again in the 08:32 run.
So one model in four has been training with NO per-era telemetry: no
per-class recall, no dW/W ratios, no zero-skill comparison, no deploy-bar
line. It was still doing the work - tier re-ranks, threshold fits and
exit-policy replays all appear on cadence - which is what made the hole look
like a grep that kept missing the line rather than a line that was never
written. It cost me the LSTM half of a gradient check earlier today.
Now a member, so each model rate-limits its own console output.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d201d22206 |
perf(baselines): the Alglib diagnostic fit the same matrix once per ensemble member
Found while costing whether to turn Run_Alglib_Baselines on. The suite builds its design from BuildFeatureWindow() over DeriveHistoryBars() bars, and neither reads any per-member state - symbol, period and feature toggles only. So every member of an ensemble builds a byte-identical matrix and fits an identical forest, MLP and OLS to it: four un-chunked fits over ~1000 columns x 4000 rows, to print one answer four times. The MI suite has had a once-per-chart gate for this exact shape since the ensemble landed; this never got one. Gated the same way, and here without the MI gate's caveat: that one has to warn that the geometry scan at the end of its chain makes a DECISION, so a donor-only run would leave the other members on a different target. This suite decides nothing - nothing trades on it, no model is saved, and ReportGeometryDrift assigns to nothing - so the donor's run is the complete answer. Solo charts are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a86379621c |
feat(labels): on a one-sided book the blocked side's class is retargeted from an entry it can never take to the EXIT of the one it holds
User request: "when an asymmetry is noticed in a market (like sp500 upward drift) ... it does not need to predict shorts, but exit points. a sell signal needs to be preceded by a buy so that it can say I predict we must close that long." Until now a LONG_ONLY verdict only BLOCKED short entries. The network went on being trained to predict them - a third of its output capacity spent learning an answer the direction policy guarantees it can never act on, while the question the book actually faces (when to get out of the long) was never asked. The two are not the same event: "a short pays" needs price to travel the SHORT's target before the SHORT's stop, and at any geometry where reward != risk that is a different bar from "this long hits its stop first". The exit is the second one. So on a one-sided book TripleBarrierLabel re-cuts all three classes around the only position the book can hold: Buy = it reaches its target, Sell = it reaches its STOP first, Neutral = the horizon expired with it still open. Both come off the allowed side's own barriers, which the walk already computed - this reads longLost where it used to read shortWon, so it costs nothing. Label lifespan and the timeout flag follow the allowed side too, so the overlap correction is sized on the window this label actually spans. DECIDED ONCE, AT ERA 0, AND PINNED. m_exitTargetSide goes in the .cfg beside the derived geometry under the same doctrine and for the same reason: it decides what Buy and Sell MEAN, and a target that moved mid-run would retrain a fitted model against something it never saw. A .cfg from before this ends early and reads 0/0 - "not decided, symmetric" - which is exactly what every existing model was trained as, so nothing needs migrating. The weights fingerprint keys on the INPUT only (explicit Long only / Short only); under Intelligent the measured verdict must never reach a filename, or the model is orphaned the moment more history downloads. THE DRIFT VERDICT HAD TO MOVE OFF THE LABELS FIRST, and it turns out it was measuring the wrong thing anyway. It counted m_labelCacheBuy/Sell and called them "always-long vs always-short win rate", but the label pair is the COLLAPSED first-touch verdict: a bar where both sides reached their target carries only the side touched first, so long wins were undercounted by the both-won-goes-to-short share. m_winLongCache/m_winShortCache are the actual per-side win rates, published before the collapse, and that is what it reads now. Necessary as well as more correct - deriving the verdict from labels the verdict shapes is a feedback loop, since Sell-as-exit is near complementary to Buy and would close the very gap that produced it. The gap's SE now leans conservative rather than anti-conservative for the same reason. LIVE. The retargeted class is wired to close the position, or training it would be pointless: CheckClosePosition's "never vote-exit a certified position" rule keeps governing symmetric books and gains a one-sided exception, and the replay reads the identical rule through one LiveVoteExitThreshold() so certified and traded cannot describe different policies. Armed only when the operator picks a close threshold (Signal_ThresholdClose ships Disabled) AND the model's own pin says its blocked-side class means "close" - a model trained symmetric never fires it, whatever the verdict has since become. This does trade a different game from the one the win-rate certificate grades; the era's EXIT-POLICY REPLAY line already reports expectancy in R for exactly this case and says so in words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2ba0f348c0 |
feat(ui): thresholds pick from a dropdown, and the finder arrows are back beside the level lines
Two UX changes the operator asked for.
THRESHOLDS. Signal_ThresholdOpen/Close were raw ints with the legal range
written in the label ("[0...100, 101 = never]") - the one input style this
codebase converted away from everywhere else. Open now takes the existing
PERCENTAGE_PRESETS, whose comment already declared itself to be "Signal_
ThresholdOpen's scale" but was never wired to it; Close takes a new
SIGNAL_CLOSE_PRESETS carrying the same rungs plus CLOSE_DISABLED = 101, which
is why it cannot just reuse the other enum. Member names are prefixed because
MQL5 enum members share ONE flat namespace - a bare PCT_25 in the second enum
would silently resolve to the first one's, warning only. Values are unchanged,
so existing .set files keep their settings. Both call sites now cast
explicitly at the CExpertSignal boundary rather than leaning on an implicit
enum-to-int conversion that only warns.
ARROWS. 2026-08-19 replaced the low/high arrows WITH trigger-price lines; that
was a swap where it should have been an addition, and it cost the zoomed-out
view. A mark is now both objects: the line is the precise entry/exit level,
the arrow off the candle's extreme is the finder that says there is something
here to zoom into. The arrow's name is the line's plus a suffix, so it stays
inside SIG_ARROW_PREFIX and every prefix-scoped purge already reaches it.
The two type-filtered sweeps had to widen or they would clear one half and
leave the other: the Hide/Show visibility loop and the pre-rescan scoped
delete both walked OBJ_TREND only. Both are typed-blind and prefix-scoped now
- the same widening this file's 2026-08-09 note describes, for the same reason
it gives. Deletes go through one WarriorDeleteSignalMark() so an arrow cannot
outlive the line it belongs to, and the sidecar deliberately still records one
row per mark off the line (the half carrying the price), with the restore
redrawing the pair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
750070c2a3 |
fix(indicators): a dead handle and a priming one both read -1, so the repair report proved nothing
The detector claimed "-1 means an INVALID HANDLE, 0 means created-but-never-
calculated". This run disproved it with our own instrumentation: the repair
line prints only when Create() RETURNED TRUE, and the depth it read
microseconds later was
BEFORE: MA=-1(h13) | AFTER: MA=-1(h13)
A freshly created, valid handle read -1 - the value the model says is
impossible for one. So BarsCalculated() < 0 does not mean "dead"; it also
covers "valid, not calculated yet", and the trigger cannot separate them.
Consequences, all fixed here:
- The AFTER depth was re-read synchronously, when it can only be -1 or 0, so
every repair looked like a failure and the line was unreadable either way.
It now reports the handle NUMBER across the recreate instead. A changed
number proves a new instance; SAME means MT5 handed back the same
refcounted one, so it was never dead.
- IndicatorDepthReport printed the handle number for MA alone. Every tunable
gets one now, through a single IndicatorDepthField() - nine near-identical
StringFormat calls collapse to one.
- The comment justifying "never release before re-creating" rested on the
claim just disproved. The decision stands, the reason is restated: given
the ambiguity, releasing is the dangerous half - a recycled number would
decrement whatever owns it now and CAUSE this outage - while re-creating a
live handle only leaks a reference on a path that fires a few times a
session.
The before-handle is captured on its own line, never as a sibling argument to
the Init* call: MQL5 does not define argument evaluation order.
Behaviour is otherwise unchanged - same trigger, same cooldown, same
recreate. Only what gets reported changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b563fca7ab |
refactor(comments): another 25 paragraph blocks in the AI base cut to stdlib style
ExpertSignalAIBase.mqh 4388 -> 4256 lines. Every edit verified the same way: non-comment lines extracted before and after and diffed, so the change is provably comments only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
21263cfd01 |
refactor(comments): Inputs.mqh and the AI base header move to stdlib comment style
Box header per function/class, section banners, and 1-3 line notes for variables - not the paragraphs that had accumulated. Both files verified mechanically rather than by eye: every declaration in Inputs.mqh (159 of them) and every non-comment line in ExpertSignalAIBase.mqh is byte- identical before and after. Variables/Inputs.mqh 881 -> 355 lines (704 -> 179 comment lines) Expert/ExpertSignalAIBase.mqh 4795 -> 4388, first ~800 lines converted What was cut is narration - the history of what a constant used to be, paragraphs restating the line below them, and commentary about earlier versions of the comment itself. What was kept is every constant, every measured number, and the traps worth a warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef6d62c604 |
fix(ensemble): the era barrier read healthy startup work as a dead member
Reported symptom: one member at era 17 while the rest sat at era 2, with
the combined vote never scoring. Two faults compound to produce exactly
that, and neither needs a broken model to trigger.
FIRST - BUSY WAS READ AS STUCK. BarrierEraHeartbeat() decides liveness
from one signal: has m_eraCount changed in the last 12 minutes. But
Train() returns early, before the era loop, for three ONE-TIME phases
that never touch m_eraCount - the label-cache prebuild, the pattern-DB
backfill and the OOS simulation walk - and those are precisely what a
slow topology spends its first many minutes doing. A member grinding
steadily through a prebuild therefore looked identical to a dead one and
was dropped from the barrier at startup, before it had trained a single
era. The constant's own comment states the flawed premise: "comfortably
past the slowest healthy ERA on the deepest chart" - true, and not the
question being asked. Those three branches now call NoteBarrierProgress()
and a chunk of phase work re-arms the watchdog exactly as an era does.
SECOND - EXCLUSION HAD NO BOUND. Once dropped, a member is skipped by
EnsembleMinTrainingEra(). Drop every OTHER member and that loop finds
nothing to take a minimum over, falls through to its `return m_eraCount`
fallback - the CALLER'S own era - and EnsembleEraBarrierHolds() evaluates
`era > era`, false, for everybody. The barrier silently becomes a no-op
and the fastest member runs away unbounded. EnsembleMinEraAnyMember()
now measures against every still-training member, excluded or not, and a
member may lead it by at most ENSEMBLE_MAX_ERA_LEAD eras.
The cap is deliberately a real stop rather than a warning. A
desynchronised ensemble is not a degraded one: the combined-vote score
and the joint checkpoint both require every member on the same era, so
weights trained past the cap can never be certified by any gate. The
hold reports which of the two it is, because the operator's next move
differs - an ordinary barrier hold resolves itself, a lead-cap hold names
a member that needs diagnosing and will not resolve on its own.
Not yet explained: "only one NN listened to the stop command". The panel
now dispatches down the filter tree and reports the count it reached
("training stopped (N model(s))"), so the next run answers that
definitively instead of leaving it to inference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
45af5b9c1e |
refactor(training): delete the minority-replay machinery that stopped running in July
Two arrays parallel to m_isTrainQueue existed only to serve replay: a
per-occurrence sample weight, and a "count this bar once" flag that kept
the reported IS accuracy on the natural class distribution while backProp
trained on the oversampled one. Replay was removed on 2026-07-31 - every
bar has been queued exactly once since - and the scaffolding was left
standing, provably constant:
m_isTrainQueueWeightScale[] - written 1.0 at both queue sites, swapped
through the Fisher-Yates shuffle to stay in lockstep with nothing,
read into a variable passed to backProp, whose own default is 1.0.
m_isTrainQueuePrimary[] - written true at both sites, swapped the
same way, and read as two guards that could not be false.
Also a `for(int rep = 0; rep < repCount; rep++)` around a hardcoded
repCount = 1, and both arrays preallocated at totalIter * 4 - about
1.5MB per model of always-constant data, on a six-core box that trains
four of them at once. Behaviour is unchanged by construction: every
removed read had one possible value.
m_maxClassSampleWeight goes with them - declared, initialised to 1.5 in
the constructor, read nowhere, and documented as "currently unread by
that path... kept in case a smaller, additive loss-level nudge is ever
reintroduced". That is the definition of YAGNI, and its 17-line comment
described the class-balance correction as data-level oversampling, which
has not been true since July.
Comment pass on ExpertSignalAIBase.mqh, -164 lines this session with
every constant and measured number kept. One correction worth naming:
the header carried 15 lines arguing for HARD 0/1 one-hot targets and
explaining why smoothing was no longer needed - directly above
LABEL_SMOOTH_HIGH 0.9 / LABEL_SMOOTH_LOW 0.05, which every training path
actually uses. It described the opposite of what ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
506626b381 |
feat(panel): commands reach signals down the filter tree, not through a registry
The control panel drove training by looping g_aiSignals[] - a
hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had
already dropped an ensemble member on the floor once (
|
||
|
|
8c0186c850 |
refactor(signals): AI signal files are identity + topology, nothing else
Every AI signal repeated the same five-line InitIndicators override that did nothing but call InitNeuralNetwork. The cause was an access mismatch, not a design: CExpertSignalCustom declares InitIndicators public, the AI base redeclared it PROTECTED, and each subclass had to redeclare it public to be reachable by CExpert. Worse, the base's own override does a different job entirely - it creates the OHLC/ZigZag feature indicators - and InitNeuralNetwork called it back scope-qualified to stop the virtual dispatch landing in the subclass. Two jobs, one virtual name, and a recursion trap held off by a scope qualifier. The feature-indicator step is now InitFeatureIndicators() (protected, non-virtual, named for what it does) and the AI base carries the single public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their copies and are now purely identity plus topology, which is the classic signal file's shape. Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant and every measured number kept. Three claims in the tier block were stale and inverted - it named CalibratedConfidenceMagnitude() as the tiering input where the code deliberately uses the RAW magnitude, and it described the signal DB as re-ranking each tier when ApplyPatternWeight declines the DB from the end of era 1. Also dropped a paragraph whose subject was a previous version of the comment, and moved two notes down onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines and three unrelated defines away from its own text). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4346dd3c24 |
refactor(stdlib): the vote thresholds are ints on the library's scale, not "confidence %"
The MECHANISM was already stdlib and is untouched: ThresholdOpen() ->
m_threshold_open, tested as `m_direction >= m_threshold_open` exactly as
CExpertSignal does it. What was wrong was the presentation. Both inputs
were preset ENUMS labelled "Min confidence to open/close (%)", which
names the wrong quantity - m_direction is a WEIGHTED MEAN OF PATTERN
WEIGHTS, not a probability, and nothing in this path is a confidence.
They are now plain ints named the way the MQL5 wizard names them:
input int Signal_ThresholdOpen = 25; // [0...100]
input int Signal_ThresholdClose = 101; // [0...100, 101 = never]
Values are exactly what shipped, so behaviour is unchanged. 101 rather
than the library's default of 100 for close: a weighted mean of pattern
weights cannot REACH 101, which is how the shipped config disables the
vote exit, and quietly lowering it to 100 would re-arm a live exit route
as a side effect of a naming change.
VOTE_CLOSE_PRESETS is deleted (its only user is gone). PERCENTAGE_PRESETS
stays - MinRecall genuinely is a percentage.
** ACTION NEEDED ON DEPLOYED CHARTS: the inputs are RENAMED, so saved
.set files no longer match and charts fall back to the defaults above.
Those defaults are the current shipped values, so a chart on 25/Disabled
needs nothing; a tuned one does.
Comment cleanup in the same pass, and this part was not cosmetic - three
blocks documented mechanisms that no longer exist:
- the AI early-exit route (deleted in
|
||
|
|
97f0631e28 |
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
|
||
|
|
7584f119e2 |
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is better". This measures that instead of assuming it, and the measurement is worth more than the swap would have been. The forest and the linear fit test whether the MATRIX carries direction. Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a long-standing independent implementation of roughly what our own dense stack computes, trained on the identical rows through the identical gate. That distinction is not hypothetical here - two silent implementation faults have already invalidated every model-based negative behind them (a transposed dense hidden gradient, so backprop was never backprop; and Adam storing sqrt(v) and feeding it back as variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our own net cannot on the same rows, the fault is in our code. Trained BEFORE the target column is rewritten for the linear fit: MLPCreateC1 builds a softmax classifier whose trainer wants the class index, which is what the matrix still holds at that point. Scored by argmax with Neutral abstaining, exactly as the forest is. Sidak family is now 3, not 2. Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP is feed-forward only (no conv, no recurrence - so it could not stand in for three of the four members at all), and it has no batch norm, no logit-adjusted loss for the class imbalance, no era resumption, no OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a maintained architecture for a control. Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay at the 0.001 the ALGLIB docs recommend when you have no reason to pick another. A wide net would spend the six-core box's hours answering a question about capacity when the question is about implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
19e3595e20 |
fix(build): g_eta - the learning rate global no longer shadows a stdlib local
MetaEditor: "declaration of 'eta' hides global variable" (Math.mqh:792
vs Network.mqh:80). The standard library's Math\Stat\Math.mqh declares a
local `double eta` in its incomplete-gamma branch, and our bare global
of the same name is in scope there.
Same fault as the b1/b2/lr/momentum macros retired in
|
||
|
|
561c88e6d4 |
feat(baselines): geometry-drift check on the derived stop
DeriveBarrierGeometry() reads the stop off a quantile of the adverse excursions in the IS region ONLY - correctly, since a geometry chosen with the holdout in view has used the holdout for selection and it stops being a holdout. The cost of that correct choice is that nothing ever checked whether the distribution it measured still holds on the bars the model actually trades. If adverse excursions run wider in the OOS window than in the IS region, the derived stop is too tight for the market it is used in, every label was cut on the wrong geometry, and the deploy gate certified a game the trade is not playing - the 2026-08-09 geometry mismatch arriving through drift rather than through a config error. Reported in TWO currencies deliberately. A rank-test p says whether the distributions differ; it does not say whether anyone should care. The stop each half's own quantile would derive says exactly that, in ATR multiples - the units the order is placed in. A significant p with both stops on the same ladder rung is a curiosity; half an ATR of movement is a problem whether or not it clears 0.05. Declustered first, same as the regime test: overlapping labels are not independent draws. Harvest guards are copied from the deriver's own so the two describe the same sample - and the split is verified identical (totalIter == bars - historyBars, so Train's oosCutoff and the deriver's are the same number). Runs before the two model fits and needs only the excursion caches: a chart too thin to fit a forest can still have drifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90c6e26e94 |
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
36254dcd55 |
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under the same Run_Alglib_Baselines switch, so they cost nothing by default and share one walk of the feature pipeline. REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis). The alt-data block already produced this finding once, by hand: 13 features x 16 window slots = 208 inputs spanning ~11.5 effective dimensions, and the cost was not wasted parameters but a gradient weighting bias, since batch norm rescales collinear copies without decorrelating them. This generalises that measurement to every column instead of the one block someone thought to check. Run on the ANCHOR BAR'S ROW, not the full window. The window is m_historyBars near-copies of the same columns at different lags, so per-bar redundancy is the question worth asking - the lag structure is what the conv/LSTM stage exists to exploit - and it keeps the eigensolve at ~60x60 rather than ~960x960. REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own signed margin: its conviction, signed by whether the conviction paid, older half of the OOS window against newer. DECLUSTERED FIRST, which is the whole reason this is not a one-line call. Triple-barrier labels overlap, so consecutive scored bars share the price action that resolved them and are not independent draws. Feeding all of them to a rank test yields a p computed on an n it does not have - the same error label overlap invalidated everywhere else here. One sample per label horizon costs most of the rows and buys a p that means something; when too few survive, it says so and declines. Both report and neither acts. Wiring the regime flag to position sizing is the obvious next step and the one to take only after watching the number: run every era on ~60 effective observations, a 0.05 test crosses by chance regularly, and an auto-derisk on that is a random position size generator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5cf706dfd6 |
refactor(kiss): lift the model fingerprint out of InitNeuralNetwork
InitNeuralNetwork() was 673 lines and the fingerprint assembly - the single most audited block in the file, since its hash decides when a trained model may be resumed and when it starts again from era 0 - sat in the middle of it with no name of its own. BuildModelFingerprint() is now that block, moved line for line. Its header states the two rules the per-field notes have been repeating one at a time for months: measured quantities never enter (the .cfg carries those, adopt-don't-compare), and new fields append conditionally so shipping one does not re-key models that never use the feature. The assembly is byte-identical - verified by diffing every `fp =`/`fp +=` line against HEAD, which differ only by the new call site. So no existing .nnw/.cfg re-keys and nothing retrains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
786aa76083 |
refactor(dry): one shrinkage estimator for classic ladders and AI tiers
The Beta-prior arithmetic that turns counts into a ranking weight was written twice, term for term: WinRateFromCounts() for the classic pattern ladders and RankTiersFromOos() for the AI confidence tiers. Same formula, two transcriptions, and the same class of duplication the binomial SE consolidation removed a few commits ago. ShrunkRatePct() in System\BinomialStats.mqh is now the only copy. The two call sites keep what genuinely differs - the classic path passes RAW trade counts with a prior of MIN_TRADES_FOR_WIN_RATE, the AI path passes OVERLAP-CORRECTED effective counts with TIER_PRIOR_EFF_N, which is far smaller precisely because effective counts are - and that contract is now stated once, in the function, instead of being implied by two comments that could drift apart. Also fixes a difference the consolidation exposed: with an empty sample and a prior present, the posterior mean IS the prior, and returning 0 there would have handed a tier a vote weight of zero on no evidence. The AI path could reach that (effN can round to 0 when labels overlap heavily); the classic path cannot, since it returns NO_DATA_WIN_RATE first. Corrects a stale note of my own in passing: this ranking was recorded as a "raw win rate behind a MIN_TRADES cutoff heuristic". It is not, and has not been for some time - it is already a proper empirical-Bayes estimator with a per-filter pooled prior. Replacing it with a significance test, as that note implied, would have swapped the estimator the weight needs for a gate answering a different question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ce2c324527 |
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture family, so "flat" has two readings that no topology tuning can separate: the net is the wrong learner, or the matrix carries no directional information. Two learners with completely different inductive biases - Alglib's random decision forest and an ordinary least-squares fit - now train on the SAME feature windows (BuildFeatureWindow, the net's own function, so there is no second feature implementation to drift), the SAME labels, the SAME IS/OOS split with both purges, and are scored through the SAME precision-against-always-one-direction comparison and the same Sidak family-wise arithmetic the deploy gate uses. If both also land at chance, the matrix is the limit. Deliberate choices, each of which could have made the comparison a different question wearing this one's name: - LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and a baseline handicapped by a forced zero intercept would flatter the net for the wrong reason. - Raw call counts in the SE, matching the live gate's known-permissive test rather than correcting it here - both sides must face the same bar. - No threshold sweep on the linear fit: a threshold fitted on the slice being scored is the calibration leak the purged band exists to avoid. - Uniform stride when a cap bites, not the newest N rows, so a score difference cannot be a regime difference. What was dropped is logged. Ships off (Run_Alglib_Baselines = false): it is a measurement, not a trading feature, nothing trades on the answer and no model is saved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
38a12a240b |
refactor(kiss): drop the AI sub-vote early-exit route; certified == traded
First of the AI vote layers to go. CheckClosePosition had two exit routes: the stock blended vote, and an AI-only one reading the AI members' sub-vote undiluted. The second existed because an AI reversal averaged in with the classic filters could be diluted below the threshold before it could close a position. It is gone, and with it m_lastAiVote and the aiResult/aiWeightSum pair Direction() carried to feed it. This CLOSES the certified-vs-traded gap rather than widening it. The deploy gate certifies a win rate measured on hold-to-resolution outcomes, and CheckClosePosition already gated the blended route off whenever an AI model's derived geometry was on the order - so the AI route was the only vote exit an AI-certified trade could take, and the exit replay existed to reproduce it. With it removed, an AI-certified position holds to its barrier by construction instead of by reconstruction, so Warrior_EA.mq5 now pushes ExitPolicy(0.0, true) unconditionally. Previously it forwarded Min_Vote_Close and relied on Disabled arriving as 1.01 to switch the simulated exit off by arithmetic - correct at the shipped default, and one input change away from the simulation and the live path describing different games. Min_Vote_Close keeps its meaning for the classic route and is now documented as inert wherever an AI certificate governs, rather than appearing to drive an exit it can no longer reach. Comment debt cleared while here: a tombstone block for m_ai_exit_threshold (a member deleted 2026-08-18) still sat in the header, and four sites still named LiveSignedConfidence's "two consumers" - it had one, the intelligent trailing stop, since that same date. NOT touched, and deliberately: NMS declustering is NOT a quality layer. It gates the live signal at Inference.mqh:226 (NmsLiveAccept), and the undeclustered population is ~8x what the EA trades. Removing it would multiply live position count, not simplify a scoring path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
77594ef5fb |
refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
29c82ad50b |
refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two deploy gates, the two edge floors, the collapse recall floor, the barrier rung ladder, the inference bin SE, the pooled inverse-variance weights and both detectability reports. System\BinomialStats.mqh now holds it once, as free functions with no class dependency, so the god-class declaration does not grow to host pure math. BinomialVar(p, n) p(1-p)/n BinomialSEPct(p, n) 100*sqrt(p(1-p)/n) BinomialCallsForEdge(p, edge, sigmas) the same, solved for n NormalUpperTailQ(z) Q(z), via Math\Stat\Normal.mqh SidakFamilyP(z, N) 1-(1-Q(z))^N Value-preserving by construction: rates go in as probabilities so no call site gained a *100/100 round-trip, and BinomialSEPct is written through BinomialVar so the multiply order is the one it replaced. Every degenerate guard each site carried (p<=0, p>=1, n<=0) now lives in one place and returns the 0 those sites already treated as "no bar to clear". CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it. What consolidating SURFACED, and is deliberately NOT changed here: the two Sidak selection gates compute their SE on the RAW call count, while every other SE in the project deflates by EffectiveSampleSize() for triple- barrier label overlap. That makes them the most permissive test in the codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live deploy bar, which is a policy decision, not a refactor - flagged in the code at both sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ea2552efe2 |
refactor(stdlib): adopt Math\Stat for the deploy gate's normal tail; retire the b1/b2/lr/momentum macros
The gate's NormalUpperTail was a hand-rolled Abramowitz & Stegun 26.2.17 approximation. Its own comment gave the reason - "drags a chain of headers behind it" - and that turned out to be one file: Math\Stat\Normal.mqh includes only Math.mqh, which includes nothing. Swapped for Cody's rational approximation in the library (~18 significant digits vs |error| < 7.5e-8). No past verdict changes: at the z the gate operates on, the difference is orders of magnitude below DEPLOY_FAMILY_WISE_ALPHA. Adopting it needed the four bare macros in AI\Network.mqh gone first. "#define b1 AdamBeta1" collides with an identifier in Math.mqh, so the include would have macro-expanded the library's own local and failed to compile - the same landmine that made the original author rename the approximation's coefficients to ntB1..ntB5 rather than use the reference's b1..b5. lr, b2 and momentum are the same class of hazard: single-token global macros in a 52k-line codebase. All four now resolve to the input names they always aliased, which is a pure textual identity - verified zero bare occurrences remain. Also: - SelectionSort over the buffered signals was O(n^2) with an O(n^2) count of StructToTime calls, because the comparison rebuilt both datetimes from the six int date fields every time. Now materialises the keys once and does an insertion sort; ArraySort cannot permute a struct array. IsEarlier goes with it, MakeDateTime becomes SignalTime. - Seven FileOpen sites lacked FILE_SHARE_READ|FILE_SHARE_WRITE, including AtomicWriteBegin, which stages every model save. All 43 sites now carry them - an exclusive open fails outright when another process holds the path, which here has meant a silently skipped save. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
61c0d19ca9 |
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift
MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
552edb5fb1 |
refactor(yagni): drop 13 accessors nothing called; unify the ATR trailing pair
Verified dead by grep across all first-party sources (references/, Scripts/, research/ excluded): EraCount, HiddenLayersCount, LstmHiddenSize, ConvFilterCount, HistoryBars and MinTrainYear setters, PendingBatchSamples, getPrevOutIndex, BaseCurrency, QuoteCurrency, CurrencyCount, IsLoaded, LastFiredDirection, DBConfidence, SpecIndex, and the conv Step/WindowOut shape accessors. Every backing member stays - each is still read internally and several are pinned by the positional .cfg layout - so this removes surface, not behaviour. Two comments were asserting the opposite of the code and are now true: the "No setter: the taper's endpoints are derived" note was directly above three setters, and the conv shape block claimed EnforceTopologyContract reads all three accessors when CNet::FirstConvWindow only ever calls Window(). CTrailingATR::CheckTrailingStopLong/Short were byte-identical but for Bid vs Ask and the isLong flag; both now delegate to one CheckTrailingStop body. Deliberately NOT removed: the fractal-target branch (TrainTargetFractal, IsFractalTarget and their label machinery). It reads as dead because the TrainingTarget input was withdrawn, but Warrior_EA.mq5:836 documents it as a parked option with a three-line restore path - that is a product call, not a refactor. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2be2970434 |
fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived capacity decision spent that: first-layer width, conv filters, LSTM hidden size. But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line on the same run already reports those bars are worth ~1210 independent observations. Sizing a network against RAW bars while grading it against EFFECTIVE ones is two subsystems disagreeing about one sample, and it disagreed in the dangerous direction because the capacity side was the optimistic one: the warning's "roughly 1.1 weights per training bar" is nearer 11 per independent observation. EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all of them statistics. This adds the ninth, in the one place that decides how many parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call sites, because that function exists precisely so the three stages spend one budget. SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured something, so on a model's first build - before any label exists - the deflation is correctly the identity: an unmeasured overlap must not invent a shrink. A fresh attach constructs a fresh object, so its counters are zero too; only a mid-session weights reset carries real evidence into a rebuild. That is deliberately safe (no attach can now re-derive a narrower topology and discard trained weights) but it would have left the first build - the case you most want the truth for - quoting the flattering figure. So ReportDetectability now restates capacity against the effective sample at the first moment L is real, for the topology already pinned. It re-sizes nothing; it reports what was bought. Placed ABOVE that function's break-even guard on purpose - a degenerate geometry is exactly when you want to know the net is over-parameterised, and "it only fires for sane configs" is how the 2026-08-18 IS-error stop managed never to fire at all. The warning also names its basis now (independent observations and L, or an explicit "overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never again read as a measured one. Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT charges for exactly what the capacity DECISION charged for - same reason RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them. Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize -> EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments). NOT COMPILED - user compiles in MetaEditor. |
||
|
|
caad156464 |
fix(geometry): the ratio raise was reported against reachability, not bounded by it
First clean derivation after |
||
|
|
6f2def0605 |
fix(geometry): a weights reset could never change the barrier - the pair laundered itself through the wipe
Reported as "it still seems leaned towards 2x atr" after a full Delete && Reset Weights on SP500 H4. It was not the .cfg pin, and it was not the new ratio floor failing to take: the geometry is held in members ResetWeights never cleared, so a reset wiped the .cfg, built a fresh net, restarted at era 0 - and then relabelled under the PREVIOUS model's pair, before SaveTopologyConfiguration wrote that stale pair back into the brand new .cfg. Reset, re-derive, re-pin, with the middle step missing. No number of resets could ever have moved it. Evidence in the 2026-08-19 journal: RESET WIPE at 16:23:38.699 (.cfg=deleted), "rebuilt a fresh topology" at .790, and a label cache at .890 with a distribution byte-identical to the pre-reset 2.00/2.00 one (Buy 3221 / Sell 3303 / Neutral 4837, mean lifespan 6.4 bars) - 100 ms later, with no DeriveBarrierGeometry between them. The member that actually blocked it is m_geometryDerivePasses. LoadAndCompareTopologyConfiguration pins it to BARRIER_DERIVE_MAX_PASSES to block the fixed-point iteration, which is correct for a LOAD - an existing model must never re-derive or the target moves under fitted weights - and exactly wrong for a RESET, which is the act of declaring there are no fitted weights left to protect. State that is correctly sticky for one lifecycle event, silently inherited by another; the same shape as the .cfg pin sitting beside it. ResetWeights now returns the whole derivation to its constructor state: the three latches, the pair, the horizon and its flags, the swing/lifespan measurements, the scan's own outputs, and m_spreadAtr - that last one matters because the cost filter is deliberately inert on pass 1 (m_spreadAtr <= 0.0) and an inherited spread makes a reset model walk a different ladder than a genuinely new one. m_sl_mode/m_tp_mode go back to the SL_Mode/TP_Mode inputs, since the adopt path overwrites them in place with no copy of what was configured. NOT COMPILED - user compiles in MetaEditor. |
||
|
|
c3daded397 |
feat(geometry): 1:2 becomes a FLOOR the swing legs may raise, and the scan can no longer undercut it
Two coupled changes, both from measurements in today's SP500 H4 log. 1. THE SCAN WAS OVERRIDING THE DERIVER ON THE WRONG OBJECTIVE. At 13:37:47 DeriveBarrierGeometry produced stop 1.21*ATR / target 2.41*ATR - break-even 33.3%. Thirty-seven seconds later the barrier-geometry scan adopted 2:2 - break-even 50.9% - because it carried 0.0143 nats of entry-time information against the configured pair's 0.0075, and cleared its family-wise gate. Information is not expectancy, and the scan says so itself; nothing checked what the adoption did to the operating point. It did this: the fitted thresholds immediately after read 38.8% win vs 50.9% break-even (-12.2pp) and 48.3% vs 50.9% (-2.6pp), where the earlier model on this instrument at a 1:2 geometry had fitted +1.8pp. The deriver applies the ratio as user RISK POLICY; a scan that can crown 1:1 makes two subsystems disagree about one geometry - the same split this file already fixed once for the clamped-horizon rule. The scan now enrols and crowns only pairings at or above the floor; sub-floor pairs are still scored and printed (marked 'r') so the choice stays auditable. This is NOT the min-RR rule removed on 2026-08-09 - that one guarded a rejection filter that no longer exists. 2. THE RATIO IS A FLOOR, NOT A CAP (user: "the ratio of 1:2 is a minimum that I want, but it should not cap to that if the average zigzag moves gives more room"). BARRIER_TARGET_RR -> BARRIER_TARGET_RR_MIN. ComputeBarrierHorizonBars already scanned ZigZag pivots for leg DURATION; it now harvests leg RANGE in the same pass - two properties of one object, so the horizon and the target describe the same legs instead of two windows. The per-rung ratio is the floor raised toward median-leg/stop, snapped DOWN to a coarse ladder (2.0/2.5/3.0/4.0/5.0). The ladder is coarse on purpose: PooledGate pools only instruments whose structural break-even matches, and continuous per-instrument ratios would never match and would silently empty the pool. A leg is the right yardstick precisely because it owes NOTHING to the barrier - sizing a target off travel measured over the barrier's own horizon is the circular loop that ran EURUSD/USDCAD away to 14-31*ATR in 2026-08-07. The raise stays bounded by the three tests already in the ladder: reachability, the horizon ceiling (first-passage time grows with stop x target), and the cost fraction. Consequential fixes: the reachability floor was a macro keyed to the fixed ratio and is now BarrierMinReachPct(rr) evaluated per rung (a raised ratio has a lower break-even, so a fixed floor would be the wrong strictness); the detectability break-even likewise; PooledGate now writes and matches the ACTUAL ratio (TargetRR()) rather than the floor. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
888f32d21c |
fix(gate): the plateau shortcut re-ran the deploy test every era, raising its own bar
User report: 'eras since best' in the ensemble line is always 0 (era 147, best at era 90,
'0 eras ago'). That is a control-flow bug wearing a display symptom.
Once every member's in-sample error had plateaued, the shortcut forced the ladder to its
DEPLOY stage on EVERY era. The failed-gate branch resets the stage to 0 so the ladder can
climb again - so the shortcut raised it, the branch cleared it, forever. Three consequences,
only the first of which was visible:
- g_ensErasSinceBest was reset every era, pinning the counter at 0.
- The stage-1/2 boosted warm restarts were never reached, so the one mechanism that can
un-plateau a stuck member never ran. The models sat at a WORSE error than their best
(0.2408 -> 0.3015 on PAI) with no escape.
- Every repetition ran EnsembleSurvivesSelection against an unchanged best and incremented
the candidate-era count the family-wise correction divides by. The run spent its time
RAISING ITS OWN SIDAK BAR - the same waste as the 2026-08-18 inert IS-error stop, one
layer up, and the reason a gate that needed >47.8% saw its bar climb era after era.
Fix: the shortcut fires ONCE PER BEST-ERA (g_ensGateTestedEra, stamped before the outcome
branches because it is the re-running that inflates the family, pass or fail). A refused
gate now falls back to the normal counter-driven ladder - warm restart, anneal, then a
fresh deploy test - which is the escape the shortcut was skipping.
Also, per user: the signal marks were too small to see. Span doubled (2.6 bar widths, so
the overhang either side of the candle is ~0.8 bars) and both layers thickened - 1px dotted
was invisible on a candle chart at any realistic zoom.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5e0317f09d |
feat(chart): signal marks become price LEVELS at the trigger, not arrows beside the candle
User request: 'move from arrows on lows and highs to small horizontal lines at the actual prices the entry/exit would trigger, just a bit larger than the candles. dark green for buy, dark red for sell.' Every mark is now an OBJ_TREND segment with both anchors at one price and both rays off, spanning 1.3 bar widths, drawn at the bar's CLOSE - the price a market order actually fires at, and the exact entry TripleBarrierLabel assumes. It used to sit on the candle's LOW for a Buy and its HIGH for a Sell: prices the trade never touches, picked so an arrow glyph would clear the candle. The tooltip now carries that price too. COLOUR NOW MEANS DIRECTION AND ONLY DIRECTION on every layer (dark green / dark red). Layer moves to width+style - the traded vote is solid and thick and drawn in front, a single model's raw opinion is thin, dotted and behind the candles - which keeps the distinction the old palette existed to draw (a model's opinion must never read as a trade) while freeing colour to say one thing consistently. Consequences handled, all of them the same 'a typed scan went blind' failure: - SaveChartSignals filtered OBJPROP_TYPE == OBJ_ARROW and read OBJPROP_ARROWCODE. It now filters OBJ_TREND and recovers direction from the colour. The sidecar keeps the old 217/218 numbers as its buy/sell token deliberately, so existing .arrows files still load. - AdvanceChartSignalRestore now rebuilds through the SAME creation point the live path uses, so a restored mark and a fresh one are identical objects. - The rescan-scoped delete enumerated ObjectsTotal(OBJ_ARROW) - retyped, or it silently deletes nothing. - ApplySignalsVisibility enumerated OBJ_ARROW with NO prefix filter. Under the new type that would have hidden and shown THE USER'S OWN trend lines on every Hide/Show click; it is now prefix-scoped. The old type was uncommon enough on a real chart to mask the missing check - trend lines are the most hand-drawn object there is. - DrawObject's high/low parameters are gone (6 call sites pass m_Close instead), so no caller can hand it a price it no longer draws at. - Fixed a pre-existing stale comment that still described the purge sweep as OBJ_ARROW-only three lines above the note explaining it had been widened to every type. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6717509c8b |
fix(build): four compile faults - one was a SILENT enum collision that inverted the direction policy
Reported by the user's MetaEditor compile of |
||
|
|
f64e0f8b67 |
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate
User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1445f175ce |
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as TCAdjustStops does at order time - the M5/tight-ATR case where live trades ran wider geometry than training measured. Current stops level stands in for history (like the spread); measured quantity, so it does not key the fingerprint. 2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which RESCANS the label cache and now runs at every era end beside RankTiersFromOos - era-cadence instead of waiting for rare full rebuilds. Prints only on change. 3. Session filter is any-broker: sessions defined on their financial centres' civil clocks (London 08-16 Europe/London, NY 08-17 America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each centre's own computed DST rule (EU last-Sun-Mar/Oct, US 2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED server-vs-GMT offset (half-hour brokers included). Windows may wrap midnight in broker time - the interval test handles it. Replaces the EET-hardcoded anchors, which were correct on exactly one broker and got Tokyo wrong by an hour each European summer. 4. The current-session-table-for-history caveat resolved by analysis: the bars bound the error - a too-late assumed close meets no bars (zero error), a too-early one truncates conservatively (<=1h, never optimistic, cannot manufacture edge). Documented at the site. 5. The ensemble deploy gate mirrors the direction policy: blocked-side fires are not fired bars (certified == traded), the zero-skill reference uses only ACHIEVABLE baselines (always-short is not a strategy a long-only book can run), and one-sidedness BY POLICY is not degeneracy - the two-sided requirement applies only when both sides are allowed. Sell predictions keep their other jobs (exit triggers, consensus dilution) untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b63e39f026 |
refactor(time): broker time throughout - and the GMT DB basis was already a live bug
User decision: "stick to the broker's time throughout the codebase and analysis, session filter, programmed close time etc". Investigation found the GMT choice was not just inconsistent but broken: live journaling stamped DB rows with TimeGMT() while the online-learning backfill stamped them with BAR time (server) - two clocks ~3h apart in the same column. The newest-row duplicate guard compares them on one axis, so a live row landing within the offset after a backfill row was silently rejected as "outdated". dbVersion 3.0 -> 4.0 wipes the Signals store: the only honest reset for a mixed-basis corpus. - Direction()'s clock (stamps every journaled row, keys the per-second vote window): TimeGMT -> TimeCurrent, variables renamed so the name cannot lie about the basis. - UpdateSignalsWeights' future-row bound: same clock as the rows. - Session filter: broker-time anchors (London 10-18, NY 15-23:59, Tokyo 2-11). The GMT anchors were backwards for an EET-family broker - such a broker follows European DST, so London is DST-STABLE in broker time and moved twice a year in GMT. Tokyo drifts 1h each European summer (no DST to track) - accepted, smallest error on offer. Also fixed: inTimeInterval ignored its datetime parameter and called TimeGMT fresh - a dead parameter hiding a hardwired clock. - MetaCorpus/SignalMETA: rows pre-4.0 are GMT, broker since; the GMT->server offset scan is KEPT because it measures rather than assumes - it pins 0 on new corpora and still resolves old ones. - AltDataFetch deliberately stays on GMT: FRED/COT/EIA release schedules are external UTC-anchored events; the as-of join maps them onto server bars downstream. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1a46dfdad9 |
feat(sessions): market-hours entry gate + "Market close" close-all option, both live from the symbol's session table
Two user requests, one authority: SymbolInfoSessionTrade, read fresh on every call so DST and per-symbol schedule changes track themselves. - WarriorMarketOpenNow(): CheckOpenPosition refuses entries outside the symbol's trading sessions (Sunday reopen, index CFDs' daily breaks) - a vote can no longer fire into a closed book and collect a broker error. ENTRIES ONLY: exits, SL/TP and the scheduled close-all stay unguarded - closing risk must never be blocked by a session boundary. - CH_MARKET_CLOSE = 24 (appended, .set-safe): the close-all fires "Close-all minute" minutes before that day's LAST session close. Friday + Market close + xxH05 = flatten 5 minutes before Friday's actual close. Resolved identically in three places: the live executor (CExpertCustom::OnTick), the label walk's vertical barrier (NextScheduledCloseAll - the symbol's CURRENT table stands in for history; MT5 keeps none, and a fixed hour is wrong by more), and the fingerprint (the |CUT: token already carries hour=24, so switching to the dynamic mode re-keys the model exactly like any schedule change). Training itself is deliberately NOT gated on market hours: weekend compute is free and labels only ever exist on real bars - what the session table gates is order placement and, via the close-all barrier, what the labels may count as holdable. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |