forked from animatedread/Warrior_EA
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8c64ec7018 |
refactor(meta): the signal tree owns its gate - no global
g_warriorMetaGate was a file-scope mutable pointer, and it did not need to be. The root CExpertSignalCustom - the one CExpert actually calls CheckOpenLong/Short on - now holds the gate as a member, and children reach it through a parent back-pointer AddFilter sets on adoption. That was the last piece of the meta veto that behaved like ambient state: - CheckOpenPosition reads MetaGate() instead of a global. - EnsembleEraVerdict's replay reads the same MetaGate(). It sits deep in the training code inside an AI filter and had no route up the tree; a global WAS that route. m_parentSignal is now, and a back-pointer is safe for the same reason the gate adapter's owner pointer is - m_filters and m_gates free their children, so a parent always outlives them. - The stale-pointer hazard is gone by construction. The global had to be hand-cleared at every re-init because an input change re-enters OnInit in the same program instance and frees the old head; the root signal is new'd fresh each time, so nothing survives one. That reset line is deleted, not moved. Note what did NOT need doing: the tree already owned the meta head itself. AddFilter routes non-voters into m_gates, so it has been a gate child of the root since the S3 wiring - it was only the VETO that lived outside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3d2ee517ca |
refactor(meta): the veto is a gate, not a virtual every signal carries
Since S3 (
|
||
|
|
0cf18d0592 |
refactor(signal): name Direction()'s two side effects
Not the full split - that is withdrawn, see below. This is the part
worth having on its own: the DB journaling and the raw-view drawing were
inline in the same loop that does the vote arithmetic, so a reader had
to separate "what this computes" from "what this writes" by eye.
JournalFilterPatterns() and DrawFilterRawView() now say it at the call
site. Pure extraction, no behaviour change; Direction() drops 172 -> 146
lines.
WITHDRAWING the recommendation to split Direction() into a pure vote
plus its side effects. The operator's question - why split it when the
stdlib already supports configurable weights and prohibition signals -
is right, and my justification did not survive it. Stdlib's Direction()
is already pure; ours is a transaction because WE added journaling,
drawing, an intra-second window, one-shot vote consumption and a
readout on top of it. So the split would only remove what we added.
That was worth doing when the vote ARITHMETIC was also duplicated. It no
longer is:
|
||
|
|
14f7718d35 |
refactor(signal): only true signalers are filters - META becomes a gate
Operator's call: "META should be removed or implemented directly into
CExpertSignalBase. Only true signalers needs to be filters."
A meta head never votes - its Long/ShortCondition are structurally 0 and
its verdict reaches the pipeline through LiveMetaGate(), not through the
vote. Keeping it in m_filters meant every consumer of that list needed a
special case, and each one was a bug waiting: VoteCapableWeight() had to
return 0 for it or it would park a permanent abstainer in the consensus
divisor. The replay's divisor bug (
|
||
|
|
7a8c759ef8 |
docs(vote): correct the stdlib rationale - m_weight IS respected
The comment added in
|
||
|
|
616e071d8d |
refactor(vote): one normalization rule, shared by live and replay
Follows the stdlib question directly: CExpertSignal::Direction() divides
by the COUNT of participating filters, which is a correct mean only
while m_weight is its stdlib default of 1.0. We set m_weight to a
win-rate-derived trust - measured 0.27-0.29 on both live charts this
morning - so dividing by count would deflate every vote by ~3.6x: a 28%
ensemble would read 7.8 against a 25 threshold and never fire. The
divisor override is load-bearing, not decoration.
What was NOT load-bearing is having two copies of it. SVoteAccumulator
is now the only place the rule lives, and both Direction()'s pass 2 and
HistoricalNetVote() Add() into it:
- capable weight ALWAYS enters the divisor, contribution or not. An
abstainer looked and said nothing; diluting the consensus is exactly
what it should do.
- a member that could not look at all (no era-end snapshot, untrained,
or a gate) contributes no capable weight, so the caller simply never
Add()s it. That is the distinction
|
||
|
|
d81ec159ce |
refactor(vote): one aggregation rule for the historical bar, live's divisor
Answers "why not just call Direction()": because Direction() is not a
query, it is a transaction. It journals DB rows, draws raw arrows, folds
its result into an intra-second averaging window, consumes one-shot
per-filter vote state and refreshes the live readout. All of that is
wrong on a bar from three weeks ago - which is why the classic replay
has to bracket its Direction() call in a six-field SaveVoteState /
RestoreVoteState. That bracket is not a feature, it is the evidence.
Because the sweep could not call Direction(), it re-implemented the
aggregation: mask, invert, sum, divisor. And a duplicated rule drifts.
It had:
den += filter.ModuleWeight(); // consensus: capable weight, ...
while live uses VoteCapableWeight(). Those differ for exactly the
members that must not be in a divisor: a META head returns 0 from the
latter (it is a gate, structurally incapable of agreeing) and its full
weight from the former, as does a member that has not finished training.
So every reconstructed vote was shrunk by members that could never
agree, and the comment on that very line said "capable weight" while the
code said ModuleWeight.
The loop moves to HistoricalNetVote(idx, capableOut) - one place, live's
divisor - and the sweep keeps only what it is for: threshold, direction
policy, NMS, draw. 42 lines out of the sweep.
This is the first half. The second is splitting Direction() into a pure
vote plus its side effects, at which point the save/restore bracket and
the separate replay path both delete themselves and there is one
aggregation for live, replay and the ensemble gate. Not done here
because it is the live trading path and this build is deploying.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b91c7b1f7a |
refactor(comments): box headers to stdlib length
The //| box blocks were excluded from |
||
|
|
5efdb48de4 |
refactor(comments): stdlib comment style across the remaining in-scope files
Same pass as
|
||
|
|
15b75d0a73 |
fix(panel): VerboseMode dropped the era progression it was supposed to add to
The compact panel leads with "learning (era N, pass X%)". The verbose panel led
with "Study -> Era N" and then progressLine, which counts BARS inside the
running pass - it changes wording between passes and reads "Era complete" for
as long as a member sits at the era barrier. So turning VerboseMode ON, which
|
||
|
|
a71d6d1d0b |
feat(chart): signal marks in full-brightness lime and red
clrDarkGreen/clrDarkRed were hard to pick out against the candles. clrLime and clrRed are the brightest pure pair MQL5 names, and they match what the vote readout already uses for "this would trade". The colour is the direction ENCODING, not decoration - a signal line carries no arrow code, so SaveChartSignals recovers buy-vs-sell by comparing against WARRIOR_SIG_BUY_COLOR, and marks left by an older build now decode as SELL. No legacy fallback is kept, per the user: weights and arrows are wiped on every push. The constraint is written next to the defines instead, for whoever changes them on a chart that is not being wiped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
667f2bcb6b |
revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts |
||
|
|
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>
|
||
|
|
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 (
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
348492fb3b |
feat(direction): INTELLIGENT trade direction - the measured drift picks the side(s)
SQX EdgeFinder precedent (user request): adjust for the drift instead of fighting it. The 2026-08-19 telemetry found the models leaning SHORT (Buy recall 21% vs Sell 40%) against a long-favored market (always-long 34.3% vs always-short 29.5% at the adopted geometry). TRADING_DIRECTION gains INTELLIGENT = 3 (appended, explicit value, .set-safe). It resolves at runtime from the label cache's per-side win rates - the Buy/Sell shares ARE the win rates of taking every bar long/short at the REAL stop/target with spread charged. A side is dropped only when BOTH hold: the drift gap clears 2 combined SEs on the overlap-deflated effective sample (EffectiveSampleSize - labels overlap ~18x), AND the weaker side sits below cost-adjusted break-even (a side that still clears costs is kept; drift tilt alone is not a reason to refuse a profitable side). Fails open to BOTH: unmeasured, tiny effective n (<30), insignificant gap, or classic-only charts (no label cache). One resolution point - WarriorEffectiveDirection() - feeds all three gates so they cannot drift apart: CheckOpenLong/Short (live entries), the filtered-view sweep (a blocked side falls into the delete branch, mirroring live), and the vote HUD's "-> TRADE" verdict. The verdict re-derives at every label-cache rebuild, prints only on change, and is computed even when the input is not Intelligent (marked informational). NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf9a9395ee |
fix(hud): the headline word is the DECISION, not the lean
"VOTE BUY 5.9%" against a 25% bar read as an ensemble that is permanently long, when it is an ensemble that would trade nothing - one member voting BUY at weight 7 against three flats owned the word all day (reported 2026-08-19, screenshot). With the per-member lines now showing each model's individual leaning, the top line says what the bot would DO: BUY/SELL only at or above Min_Vote_Open, NEUTRAL below it (including the all-flat read), "--" only when nobody has a decision. The lean survives as a SIGNED percentage (+ buy side / - sell side). NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f30e342a1d |
feat(logs): throttle the settled per-era diagnostics - measured 22MB/9.5h of confirmed-working systems
Measured from the journal (2026-08-19): the era deep-dive line (~2KB) plus the excursion verdict, tier re-rank, calibration move, barrier hold and selection-regressed note each printed EVERY era for EVERY member - ~940 eras/member/day - long after the systems they watch were confirmed working. Yesterday's file was 1.3GB (70% of it the news-filter calendar spam the sweep fix already removed). VerboseMode returns as an INPUT (demoted 2026-08-01 for the marketplace; that track is dead since the 2026-08-16 pivot) and gains a second job: false throttles each settled per-era print to eras 0-3 plus every TRAIN_LOG_EVERY_ERAS-th (25 ~= one deep-dive per ~15min per member); true restores the per-era firehose, flippable live. Never throttled: anything that marks a CHANGE - new bests, restores + eta decays, plateau stage transitions, deploy approvals, warnings, errors, the label-cache/adoption one-shots, and the combined-vote gate line (the active system's primary telemetry, still every era). Semantic fixes over blanket gating: - barrier hold now ARMS silently and prints only when the hold outlasts the 2-min report interval - a brief hold every era is the design, the long hold is the watchdog case the line exists for; - the ensemble deploy REFUSAL prints immediately when its reason changes (that is a finding), on cadence when unchanged; - the filtered-view census prints when its RESULT moves (drawn count, or strongest vote by >=2pp) and at least every 10th sweep. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
83ae56cc9e |
feat(hud): per-member neuron lines + a vote label that moves as the nets learn
Both 2026-08-19 reports were the same staleness: every source behind the label was an ERA artifact (live cache refills at pass-3 completion, the snapshot copies once per era, dPrevSignal is the frozen purge-band edge bar) - so the readout stepped at era cadence at best, stayed glued to one direction, and lagged the era counter. DisplayInference(): throttled (4s, 1s across an era boundary), SIDE-EFFECT-FREE forward of the current decision bar (window ending on bar 1, same question the live path asks) through the LEARNER net. Batch-norm running stats are bracketed frozen/RESTORED via the new CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore, not unfreeze, because a display tick can land between pass-3 chunks whose whole scan holds them frozen. Writes nothing a trading or training path reads (dPrevSignal, NMS state, tallies, watermarks all untouched; RefreshLatestSignal is not reusable here precisely because it writes all of them). LSTM safe by construction: h/c zeroed per forward. ProspectiveVote() reads the fresh forward as its FIRST source; the era-artifact chain becomes the fallback (meta head, warm-up, window holes). DisplayHudLine(): the reference library's training label, per ensemble member - name, output activations (softmax probs or raw scalar), the decision, its weighted vote (the exact consensus numerator term), era, recent average error, "(trn)" while not vote-capable. Rendered under the vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines are telemetry, not tradable readings), coloured by the member's own direction in muted tones - the vote line's strict green-only-when-it-would-trade rule is untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1a900a0a35 |
feat(vote): CONSENSUS arithmetic - agreement is now what the threshold dials
Era-680 report, all three observations one equation: "peak 29, no arrows at
threshold 30" / "at 20, arrows on EVERY bar" / "label at 12 while arrows
everywhere". Under the voters-only divisor, any bar with at least one
directional voter read the weighted mean of the firing tiers' weights - and
once the tiers self-ranked to each model's pooled win rate (~28-31), that
mean was NEAR-CONSTANT regardless of headcount. One member alone: ~29. Four
unanimous: ~29. Min_Vote_Open was a step function around that constant -
above it nothing ever fired, below it everything did - and the label's 12
was a 3v1 split netting through the same divisor. Not three display bugs:
one arithmetic that could not express agreement.
The divisor is now the CAPABLE weight - every filter that could vote,
whether it did or not:
* live (Direction): VoteCapableWeight() - classic pattern ladders always,
veto filters never, AI members once past the same readiness test
LongCondition gates on. A model still training must not dilute an
ensemble it cannot join: four trainees + one deployed model is a solo
chart wearing an ensemble label, and the solo vote reads full strength.
* gate (EnsembleEraVerdict): g_ensVoteWeightSum accumulates for every
member that EVALUATED the bar, Neutral included.
* overlay sweep + prospective readout: weight counts whenever the member
has data; a snapshotted Neutral dilutes.
One arithmetic, four sites, same numbers everywhere.
What the numbers become (four members, w~0.29, tiers~29): unanimous ~29 -
the CEILING, which is the pooled win rate and is what the peak displays;
3-of-4 ~22; 2-of-4 ~14.5; 3v1 ~14.5. Min_Vote_Open 20 now means "roughly
three-quarters of the ensemble's trust agrees, net". It MUST sit below the
ceiling to ever fire - the census/peak states the ceiling.
This is the ensemble the user specified in the original design discussion
("if the perceptron also votes, both together reach the threshold; if
another NN votes the other side, the threshold is not reached") - union
semantics was the pre-ensemble behaviour, kept until measurement showed its
vote magnitude was a constant.
Plus overlay DECLUSTERING, the other half of "arrows on every bar": the
same three NMS rules as the per-member arrows (same-direction runs collapse
to their first bar, cross-direction flicker keeps the stronger side), online
over the sweep's strictly oldest->newest walk. Suppression is a verdict and
deletes a standing arrow; the den==0 no-data skip still never does.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c393497fd6 |
fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era
Full-pipeline analysis after "threshold 30, attained often, nothing drawn,
still glued to buy". The log falsified the premise before any code did:
21:40:43 swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0%
21:42:07 swept 4999, 0 voters, drew 0
21:51:30 swept 4999, 0 voters, drew 0
21:56:30 swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0%
The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root
cause, three symptoms: every display path read m_arrowSignalCache, which is
wiped to sentinel at each era start and only complete again when pass 3
finishes. With eras at ~30s and a sweep at ~17s:
* ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its
else-branch deleted the arrow on every voteless bar - erasing the previous
sweep's entire output. The chart cycled populated -> blank -> populated;
the user kept catching the blank phase.
* READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every
era and fell through to dPrevSignal - the frozen purge-band edge bar that
reads Buy.
|
||
|
|
4f2d81a52e |
fix(chart): sweep hammered the news filter; peak was a fossil; neutrals invisible
Careful read of the 21:14 log window (user report: peak stuck at 50, label sticky, neutrals never shown). Three distinct defects, one commit because they share the two files. 1. 15,508 "CalendarValueHistory failed" lines in 68 SECONDS - ~230/second. The overlay sweep replayed Direction() on EVERY non-AI filter, including the news/session/risk-guard veto filters. The news filter calls CalendarValueHistory per evaluation and MT5's calendar cannot answer more than ~30 days back (the known calendar cliff), so every historical bar logged a failure - real wall-clock burned inside a sweep whose whole point is to stay cheap. Veto filters keep m_pattern_count at its 0 default (the same test UpdateSignalsWeights keys on): they cast no weighted vote, and a prohibition cannot be reconstructed faithfully anyway - it joins order validation in the cannot-replay family. Skipped. Compounding it: at era ~200 the four members complete a barrier round every ~20s while a full 5,000-bar sweep takes ~17s of slices - the sweep finished and instantly re-armed, forever, against arrow caches half-rebuilt mid-era. That is why the census's "had a voter" flapped 1299 -> 257 -> 1113 across three back-to-back sweeps. Re-arms now rate-limited to one per 5 minutes. 2. Peak 50 was a FOSSIL. m_votePeak never reset, so it still held a value attained under the 25/50/75/100 DEFAULT tier weights from the attach window before the first re-rank - unreachable ever since the weights became measured (pooled 27-32 in the same log). A ceiling nothing can reach reads as "the models are underperforming their own history", which is backwards: the history was priced in different money. The peak now resets at the same regime boundary as the census (StartFilteredOverlay), and the label shows max(live peak, census strongest-vote) - the census number is the actual answer to "can Min_Vote_Open ever be reached", measured over ~5,000 bars under the CURRENT weights. 3. Neutrals were invisible. The prospective count lumped Neutral-deciding models in with voters, so "4 model(s)" read identically whether all four voted or three sat flat. Now "2 vote/2 flat", and an all-neutral bar reads "VOTE flat ... 0 vote/4 flat" instead of "--" - the models answered, and the answer was Neutral. Expected values, from this log's own re-ranks (all four members' fires land in T3; tier weights 27-32; module weights 0.27-0.32): a unanimous-buy bar reads ~29-30%, mixed membership 28-34. The reported "stuck at buy 28, climbed to 30, flashes of sell, now 33.4" is those weights doing exactly what they should. The stickiness between moves is pass 2/2.5/3 - only pass 1 writes dPrevSignal, so the label holds the last pass-1 bar's decision for the remainder of each era. Display-only, and honest: it is the model's most recent output. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b05b4f21d7 |
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after |
||
|
|
a042cb4359 |
feat(chart): show the PROSPECTIVE vote while the models are still training
The readout sat at "VOTE 0.0%, 0 voters" constantly. Correct, and useless.
LongCondition()/ShortCondition() return 0 behind the readiness gate for the
entire training run - a model that is not deployed does not vote - so the LIVE
vote is structurally zero for hours, which is exactly the period the readout
is being watched. Worse, it was the same display whether the models were
silent, undeployed, or the filter list was empty: three different situations,
one number.
When no filter casts a real vote, the readout now shows the PROSPECTIVE one -
what these models are saying right now, through the identical tier/weight
arithmetic, minus the readiness gate. That is the same quantity the historical
overlay reconstructs on cached bars, deliberately, so the live line and the
reconstructed arrows are the same measure and can be read against each other.
It can never be mistaken for a decision: labelled "-> training, not tradable
yet", drawn dimmer than "no trade", and `fires` is forced false regardless of
magnitude, because saying "-> TRADE" about a number that cannot place an order
is the precise overstatement this readout exists to prevent. m_direction is
untouched - display only, no trading path reads it.
Confirms the sweep fix from
|
||
|
|
8ab8cf6b92 |
feat(chart): on-chart vote readout, and Min_Vote_Open 50 -> 40
THRESHOLD. 40 is a measured correction, not a preference. Once RankTiersFromOos() replaced the designed tier priors with each model's real held-out win rate, the vote converges on that win rate - logged 2026-08-18 as pooled 23-36% across four members on three symbols - so a 50% bar could not be reached by anything on offer and the ensemble gate fired on 0 of 4,865 OOS bars. 40 clears the ~34% break-even those same lines report without being unreachable. The comment says plainly not to copy the number: break-even is a function of the barrier geometry, so read the gate's own "needs >N%" for the config in front of you. READOUT. One line, top-right: VOTE SELL 37.2% peak 44.1% need 40% 3 voter(s) -> no trade Every other number on the chart is downstream of the weighted mean the open threshold is compared against, and that was the one quantity never displayed. A chart with no arrows could mean the models abstained, the vote was diluted, or the threshold is unreachable - and telling those apart meant waiting for an era to end and reading the gate line, which is how the last two sessions went. PEAK is the part that earns its space. A threshold above what the vote ever attains can never fire, and that is not knowable from a single bar - it is precisely the "unreachable gate vs merely unmet gate" confusion this project has paid for twice. Colour carries the verdict rather than the direction: green/red ONLY when the vote would actually place an order, grey otherwise. Green-for-buy would make a below-threshold buy look like a trade, which is the specific misreading the display exists to prevent. Guarded on `total > 0` for the same reason the normalization is: Direction() is inherited as-is by every leaf filter, so without it each filter would write its own opinion into the one shared label and the last to run would win - the reader would be looking at an arbitrary member's number believing it was the vote. Drawn after the +-100 range check, so it shows what the threshold is actually tested against. CORNER_RIGHT_UPPER: the status lines, control panel and ensemble panel all live on the left. Registered in WarriorChartPrefixes() explicitly even though the "Warrior" catch-all already reaches it - that catch-all exists because the list has drifted twice, not to make entries optional. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
155f56e0c0 |
fix(chart): the overlay sweep compared a series index against a bar count
"Filtered view: swept 0 bar(s), 0 had a voter" on charts whose models were
reporting thousands of held-out fires in the same second - the contradiction
the user spotted in the log.
m_overlayIndex is a SERIES index (0 = newest, counting backwards in time), but
its floor was computed as `barsAvail - span`, which is a count from the OLDEST
end. Two different coordinate systems. On XAUUSD's 15,049 bars that produced a
floor of 10,049 against a start of 5,000, so `m_overlayIndex >=
m_overlayStopIndex` was false on the very first test: the sweep reported
completion having touched nothing, and re-armed and "completed" again on every
era boundary. Both bounds are now series indices - start at the oldest bar to
reconstruct, stop at 2 (bar 1 is the decision bar the forward path owns, bar 0
is still forming).
The 150-bar indicator warm-up margin was being applied to the floor, where it
could only ever be wrong; it is a cap on how far BACK the START may reach, on
the same axis. m_overlayOldest is renamed m_overlayStopIndex because with
index 0 = newest that bound is the most RECENT bar, not the oldest - the name
said the opposite of what it held.
Verified across chart sizes: 15,049 bars -> 4,999 swept; 4,865 -> 4,714;
400 -> 249; 301 -> 150.
The census line added in
|
||
|
|
129a0d448d |
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from
|
||
|
|
e59dc1629f |
fix(deinit): vote arrows survived the cheap sweep, and 5 long loops ignored the stop
Leftover chart objects on long-history charts. Two causes, one of them introduced by |
||
|
|
07aa01777c |
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from
|
||
|
|
b28c81eb78 |
feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat
|
||
|
|
4858507146 |
feat(vote): thresholds become confidence percentages, on ONE scale everywhere
User request: "the entry/exit thresholds are manual numbers, I would like
them to be confidence percentages, so the current 20 would be only 20%
confidence in a profitable trade."
WHY 20 WAS EVER SENSIBLE. Under UseDatabaseRanking both factors of a filter's
contribution are win rates: the pattern weight is that pattern's measured win
rate (UpdateSignalsWeights -> ApplyPatternWeight) and m_weight is the filter's
average win rate over its patterns, /100. Dividing the sum by the VOTER COUNT
therefore produced a mean of PRODUCTS of two win rates - a genuinely
60%-accurate filter firing a 60% pattern scored 0.60 x 60 = 36. The number was
never on a probability scale, so its magnitude meant nothing on its own.
Dividing by Sum(m_weight) instead makes it a weighted MEAN of win rates, which
is a win rate: result = Sum(w_i*p_i)/Sum(w_i). Every voter at 60% now reads 60;
MACD's double-divergence pattern (weight 100) voting alone reads 100. m_weight
stops being a discount on the probability and becomes how much a filter's
opinion COUNTS - which is what a module weight should always have been.
Default Min_Vote_Open 20 -> 50: not a tightening, the same bar re-expressed.
ONE SCALE, EVERYWHERE - the part that made this bigger than a rescale. Three
other places compared against a 0..1 softmax confidence and would each have
become a fresh currency mismatch the moment the input changed meaning:
* the AI early-exit route (LiveSignedConfidence vs m_ai_exit_threshold) now
reads m_lastAiVote - the AI filters' own weighted mean, undiluted by the
classic side, which is the only reason that route exists - against the
same m_threshold_close the averaged vote uses. m_ai_exit_threshold is
retired rather than left dangling.
* m_oosDecisionSeries now carries the vote, not the confidence, so the exit
SIMULATION stops modelling a close rule the EA does not run.
* ExitPolicy() clamped anything > 1.0 to zero. Passing the unscaled input
through that would have silently switched vote exits off in the
simulation while live went on running them - found before it shipped;
the bound now tracks the scale.
LiveSignedConfidence() is deliberately untouched and still 0..1: MM sizing,
SL/TP scaling and the intelligent trailing want a model confidence, not a win
rate.
CALIBRATION CAVEAT, stated in the code where the claim is made: this is only a
real probability to the extent the pattern weights are. A pattern with fewer
than MIN_TRADES_FOR_WIN_RATE journaled trades keeps its DEFAULT weight - a
designed prior (25/50/75/100 for the AI tiers), not a measurement. Until the
signal DB fills, "60" means "the designed conviction of the patterns that
fired". Closing that gap is the next commit.
Also corrects VOTE_CLOSE_PRESETS' comment, which documented the two scales
this removes.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
282b535037 |
feat(chart): filtered view - one arrow per trade the bot would actually take
Adds DrawUnfilteredSignals (default OFF) and, with it off, replaces the per-model arrow layer with the decision the EA would really have made. THE FILTERED ARROW IS DRAWN AT THE ORDER, NOT AT THE THRESHOLD. Clearing Min_Vote_Open is not the same as trading: a setup can pass the vote and still never reach the broker (invalid SL/TP, stops-level, ATR warm-up, unsynced swing history), and every one of those lands in OpenParams' failure branch. So DrawVoteArrow() fires only after the order parameters validate, and the failure branch withdraws any arrow already standing on that bar. One arrow is one entry the EA would have placed - carrying the vote, the threshold it cleared, and the SL/TP the order would have had. Classic signals now draw too, under their own name and weight, so a chart running MA/RSI/MACD/Ichimoku alongside the nets reads the same way an ensemble chart does. They can only be drawn from the aggregate's once-per-bar pass, because unlike the AI members they have no cached per-bar scan. Two subtleties that would each have produced a quietly wrong chart: - The raw classic draw sits AFTER filter.Direction(), not beside the journaling block. GetActivePattern*() are CONSUMING reads holding the PREVIOUS evaluation - "one tick later", which at Expert_EveryTick=false is one BAR later. Keyed off those and placed at StartIndex(), every classic arrow would have been drawn one bar early, which on a chart is indistinguishable from a model that genuinely leads. Peek*() accessors (non-consuming) let pattern, weight and bar come from one evaluation. - CExpertSignalAIBase::DrawObject() early-returns instead of gating its five call sites, so the switch cannot be honoured in three passes and missed in the fourth. Its delete counterparts stay ungated so flipping the input off and rescanning clears the raw layer rather than stranding it. SIG_ARROW_PREFIX and g_signalsVisible move from ExpertSignalAIBase.mqh down to ExpertSignalCustom.mqh - the nearest common ancestor - because the classic signals cannot see the AI header (it is included later in Warrior_EA.mq5). The vote layer gets SIG_VOTE_PREFIX under the same bare prefix, so WarriorChartPrefixes()' purge still reaches every arrow without knowing they exist. NOT YET BUILT: the reconstructed history behind attach. Filtered arrows currently start where the EA starts. See the next commit. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
62a719f04c |
fix(consistency): one geometry authority, one exit authority, and the MI screen finally gets a veto
Consistency pass before a fresh deployment. Three places where two systems were choosing the same thing and one of them silently lost. 1. THE GEOMETRY SCAN'S DECISION WAS INERT - measured, not suspected. USDJPY, 2026-08-17: 14:24:12.844 adopting barrier geometry 2:8 ... Relabelling and training on it. 14:24:12.979 triple-barrier labels - stop 1.61*ATR, target 3.21*ATR ... this is what trains It adopted 2:8 and trained on 1.61:3.21. ReportBarrierGeometryScan wrote only m_sl_mode/m_tp_mode, and BarrierMultiples ranks the DERIVED pair ABOVE those ints - so on any model carrying a derived pair (every model with a .cfg, including a fresh one whose weights are gone but whose sidecar survived) the adoption changed nothing. Worse, had it changed something it would have been undone immediately: the adoption sets m_labelCachePrebuilt = false, and that prebuild re-runs DeriveBarrierGeometry at era 0, which overwrites m_derivedSl/TpMult from the excursion quantiles. ONE AUTHORITY: the derived pair, because it is what the labels read, what the deploy gate certifies, what g_Derived*AtrMult places on the live order, and what the .cfg pins across restarts. The scan now writes THAT (floored by MIN_SL_ATR_MULTIPLIER, the same floor DeriveBarrierGeometry applies so the live stop can never be wider than the labelled one), republishes to the bridge immediately rather than at the next era end, and forces the sidecar to be rewritten. m_geometryAdopted latches it so the derive pass the adoption itself triggers cannot overwrite it. The scan outranks the derive for an evidential reason, not an architectural one: its winner cleared a permutation test against the null of the MAXIMUM over every eligible pairing, and it scores the incumbent derived pair as a peer in that same field. The derive is a descriptive quantile read with no significance test attached. BEHAVIOURAL CHANGE, and the reason to flag it before a fresh test: barrier geometry will now actually move when the scan says so. Until today it never did. 2. ONE EXIT AUTHORITY, tied to whose certificate the trade was placed under. CheckClosePosition had two routes. The AI early-exit reads the AI vote undiluted and is exactly what the new exit replay reproduces. The blended route thresholds m_direction, the average over EVERY filter including classic ones whose live votes pass 3 never computes - so it can close a position the certificate never modelled, and no replay can ever check it. When g_DerivedSlAtrMult > 0 the AI's measured geometry is on the order, which means the deploy gate's certificate is the reason the trade exists. In that state the AI now governs the exit and the blended route is suppressed. Classic-only configurations are untouched: there the blended route is the only exit opinion and stays exactly as it was. Nothing moves at the shipped defaults either way (Min_Vote_Close = Disabled). 3. EDGEFINDER, SECOND HALF: THE MEASUREMENT NOW STEERS. The MI suite has always printed its verdicts and then trained the direction target regardless of what they said. That gap IS the difference between this and the EdgeFinder discipline: measure what the market offers, THEN aim. m_dirEvidence is set when EITHER the feature/label mutual information OR the normalised excursion asymmetry clears its block-permuted null - an OR, because the two look for the same thing by different routes and requiring both would reject on the weaker of two independent measurements. Normalised asymmetry specifically, never the raw one, which is the volatility confound. Deploy - solo AND ensemble - now requires it. A run without it still trains, and keeps its checkpoint: the research value is real and the measurement can be wrong. It simply may not go live. Reported separately from the statistical gate because the remedy is different: a failed selection test says train differently, this says look somewhere else. Excursion SIZE keeps clearing where direction does not, and that is a risk-control head rather than an entry signal. For the ensemble the check is per-chart by construction - the MI suite runs once and shares its outcome across members - which is the honest treatment: four models finding nothing between them is not four chances at an edge, it is four fits to the same absent information. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
94019f363e |
feat(gate): grade OOS calls on the exit policy actually in force, and move vote combining out of the members and into the orchestrator
Option (a) from the exit-policy question: the certified number must be the traded number. Plus the modularity correction the user called for on |
||
|
|
7caf2f626e |
feat: derived taper restored; DB ranking reads a reserved slice, shrunk
TOPOLOGY - reverts the two constants and drops CausalHiddenLayerFloor. The MQL5 article's 30%-per-layer cut and floor of 20 are load-bearing on ITS first-layer width of 1000 (1000->300->90->27 needs a floor to stop). This codebase MEASURES that width, and on the live SP500 H4 config it is 16 units - already floored, with the budget printing "11360 estimated in-sample bars cannot support a 800-wide input ... roughly 1.1 weights per training bar - expect overfitting". At 16 units a floor of 20 makes lastHidden >= m_initialNeuronsCount, so ComputeHiddenLayerCount returns on its first branch and the width taper - the only part derived from this symbol's data - became dead code on all four ensemble members, with depth (2 -> 4) set entirely by counting feature domains. ComputeLayerWidths had already rejected this exact pair of constants in its own comment. The causal floor's premise does not hold either: layers are not inference steps. The "1 layer linear / 2 nonlinear / 3 multi-connected" result is Lippmann 1987 and is about hard-threshold units; with sigmoid/ReLU, Cybenko 1989 and Hornik 1991 give universal approximation from a single hidden layer. Depth buys parameter efficiency for compositional functions, not reasoning hops. ForceHiddenLayers remains for measuring depth directly. RANKING SLICE - the backfill no longer reads the window it is judged on. The deployed checkpoint is CHOSEN as the best-scoring era on the OOS window, so win rates measured back over it are selection-inflated, and the backfill was writing exactly those into the table filter weights rank on: the selection set consumed twice, beside a deploy gate that applies a Sidak correction for that effect. The newest RANK_SLICE_PCT_OF_OOS (20%) of the OOS window, plus a label-horizon purge, is now reserved and graded by nothing - not pass 3, not checkpoint selection, not the gate. The backfill reads only that. The gate keeps ~80% of its measurement (power goes as the square root, so ~10% of a sigma), and the slice is the newest data, which is the regime about to be traded. RankSliceBars returns 0 when no honest slice fits and the backfill then REFUSES and says so, rather than falling back to the scoring window and looking like a success. SHRINKAGE - per-tier win rates are shrunk toward the filter's own pooled rate by MIN_TRADES_FOR_WIN_RATE pseudo-trades before becoming weights. The raw ratio at the minimum sample count carries a ~15pp standard error, so a tier that went 8-2 was handed weight 80 and outranked a tier measured over hundreds of calls at 55 - the ranking was being driven by which small tier got lucky. Opt-in per call site (priorWeight 0 keeps the raw behaviour). Compile-verified: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
64c5dd55d3 | feat: implement one-shot pattern-database backfill and enhance accuracy tracking for ensemble models | ||
|
|
7cc6e35adc |
fix(exits): hold-to-barrier policy for fractal-target charts - live trades now match the certificate
The first-ever family-wise gate pass (SP500 D1 PAI, +10.4pp, p=0.0081) certifies a win rate measured on HOLD-TO-RESOLUTION outcomes: entry, then the measured SL or TP decides. Live, three vote-driven exit routes could close earlier - the averaged-vote close, the AI early-exit route (both in CheckClosePosition), and CheckReverse - and the fractal target's vote flips at swing-marker cadence (~3-5 bars), far inside the barrier's typical travel time (median 7-8 D1 bars to target). The user observed exactly this: an opposite arrow near an entry, trade cut, price kept going. On a fractal-target chart with a live direction model, all three routes are now suppressed (m_holdToBarrier, set in InitializeSignal, loudly logged): positions run to their broker SL/TP. Risk guards and trailing are deliberately untouched - account protection is not signal opinion. Barrier-target models keep the vote exits: their label is the vote's own horizon, so for them the routes are semantically consistent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1bf3eba68a |
feat(meta): self-contained corpus - the META chart sweeps the real classic ladders over its own history
The user should not need a tester corpus run per symbol. Every pattern
condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on
`int idx = StartIndex()` with zero hardcoded indices (verified), so a
name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes
the EXACT live ladder code answer "what would you have fired at bar i" -
the silent-divergence trap that justified the DB corpus does not exist on
this path, and neither do the GMT-offset ambiguity, the DB row caps, or
the wipe procedure.
- CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() +
SweepPrepare(bars) (deep-resizes the shared price series); the four
classic signal classes override SweepPrepare to deep-resize their own
indicator buffers.
- CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run
Direction() shifted, harvest the per-side pattern slots + netVote into
the same corpus arrays the DB loader fills; entry=bar open so
MetaPrepareEra's resolution matches at offset +0 with zero price error.
DB corpus remains the fallback when classic filters are disabled.
- Warrior_EA.mq5: META gets the enabled classic filters as candidate
sources (family ids match the descriptor one-hot).
- UseDatabaseRanking default false -> true (user request): a META chart
journals + ranks out of the box.
Workflow per symbol is now: attach ONE chart with AIType=META (optionally
Meta_ExportDataset=true for the offline pool) - candidates, labels,
training and export all happen in place, ~10 seconds of sweep instead of a
tester run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|