2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
# include <Expert\ExpertSignal.mqh>
# include "..\System\NewBar.mqh"
# include "..\Structures\tradeRecordStructure.mqh"
# include "..\Structures\signalInfoStructure.mqh"
# include "..\Variables\ConfidenceBridge.mqh"
# include "..\System\TradeChecks.mqh"
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>
2026-08-19 23:47:06 -04:00
# include "..\System\BinomialStats.mqh"
2026-08-09 14:51:59 -04:00
//--- Enumerations
# include "..\Enumerations\GlobalEnums.mqh"
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>
2026-08-18 15:41:28 -04:00
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| SIGNAL ARROW NAMESPACE - declared HERE, in the common base, and |
//| not in ExpertSignalAIBase.mqh where it used to live. |
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>
2026-08-18 15:41:28 -04:00
//+------------------------------------------------------------------+
# ifndef SIG_ARROW_PREFIX
# define SIG_ARROW_PREFIX " WarSig_ "
# endif
//--- THE FILTERED VIEW's own namespace: the combined vote, which belongs to no single filter. Sits
//--- under the same bare prefix as the per-filter arrows so one purge still reaches everything.
# define SIG_VOTE_PREFIX SIG_ARROW_PREFIX " VOTE_ "
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>
2026-08-20 13:50:33 -04:00
//--- SIGNAL MARKS ARE TWO OBJECTS, drawn as a pair for two different reading distances (2026-08-20
2026-08-22 00:25:52 -04:00
//--- user request). The LINE is a short horizontal segment at the trigger price - the precise
//--- entry/ exit level, readable only zoomed in.
2026-08-21 22:35:15 -04:00
# define WARRIOR_SIG_BUY_COLOR clrDodgerBlue
2026-08-21 21:57:21 -04:00
# define WARRIOR_SIG_SELL_COLOR clrRed
//--- THE COLOUR IS THE DIRECTION ENCODING, not decoration - a signal line carries no arrow code, so
2026-08-22 00:25:52 -04:00
//--- SaveChartSignals recovers buy-vs-sell by comparing against WARRIOR_SIG_BUY_COLOR. Half-width
//--- of the segment as a fraction of one bar.
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>
2026-08-19 15:40:52 -04:00
# define WARRIOR_SIG_LEVEL_HALF_SPAN 1.3
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>
2026-08-20 13:50:33 -04:00
//--- Wingdings codes for the arrow half of the mark, and the direction token persisted in the
//--- .arrows sidecar - one number doing both jobs, as it originally did. The sidecar stores it, the
//--- line half recovers direction from its COLOUR (it carries no code), and the arrow half draws it.
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>
2026-08-19 14:11:49 -04:00
# define WARRIOR_SIG_CODE_BUY 217
# define WARRIOR_SIG_CODE_SELL 218
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- How far back either chart rebuild reaches: the AI members' "Show signals" rescan and the
2026-08-22 00:25:52 -04:00
//--- aggregate's historical filtered overlay.
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
# ifndef SIGNAL_RESCAN_LOOKBACK_BARS
# define SIGNAL_RESCAN_LOOKBACK_BARS 5000
# endif
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>
2026-08-18 15:41:28 -04:00
//--- Panel "Hide signals" toggle (Warrior_EA.mq5). Read when creating an arrow so one drawn while the
//--- toggle is off is born hidden rather than flashing onto the chart until the next sweep.
extern bool g_signalsVisible ;
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>
2026-08-20 13:50:33 -04:00
//--- The arrow half's object name is the line's plus this suffix, so it stays inside SIG_ARROW_PREFIX
//--- and every prefix-scoped purge, sidecar scan and visibility sweep already reaches it unchanged.
# define WARRIOR_SIG_ARROW_SUFFIX " _a "
string WarriorSignalArrowName ( const string lineName )
{
return lineName + WARRIOR_SIG_ARROW_SUFFIX ;
}
//+------------------------------------------------------------------+
//| Removes a signal mark - BOTH halves. Every caller that used to |
//| ObjectDelete the line name must come through here, or the arrow |
//| outlives the line it belongs to and the chart accumulates marks |
//| for signals that were withdrawn. |
//+------------------------------------------------------------------+
void WarriorDeleteSignalMark ( const string name )
{
ObjectDelete ( 0 , name ) ;
ObjectDelete ( 0 , WarriorSignalArrowName ( name ) ) ;
}
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>
2026-08-18 15:41:28 -04:00
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| The one place a signal mark is actually created. Deliberately a |
//| free function rather than a method: four unrelated callers need |
//| it (a classic filter, the aggregate signal's vote layer, its |
//| historical overlay rebuild, and the AI members' own raw view) |
//| and only some of them are signal objects at all. |
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>
2026-08-18 15:41:28 -04:00
//+------------------------------------------------------------------+
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>
2026-08-19 14:11:49 -04:00
void WarriorPlotSignalLevel ( const string name , const datetime t , const ENUM_TIMEFRAMES period ,
const double price , const bool isBuy , const bool isTrade ,
const string tooltip )
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>
2026-08-18 15:41:28 -04:00
{
if ( t < = 0 | | ! MathIsValidNumber ( price ) | | price < = 0.0 )
return ;
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>
2026-08-19 14:11:49 -04:00
int half = ( int ) ( PeriodSeconds ( period ) * WARRIOR_SIG_LEVEL_HALF_SPAN ) ;
if ( half < = 0 )
half = 60 ;
ObjectCreate ( 0 , name , OBJ_TREND , 0 , t - half , price , t + half , price ) ;
//--- Re-applied every call, not just at creation: this doubles as the refresh path, and a mark
//--- whose price moved (a redraw at a corrected level) must move with it.
ObjectSetInteger ( 0 , name , OBJPROP_TIME , 0 , t - half ) ;
ObjectSetDouble ( 0 , name , OBJPROP_PRICE , 0 , price ) ;
ObjectSetInteger ( 0 , name , OBJPROP_TIME , 1 , t + half ) ;
ObjectSetDouble ( 0 , name , OBJPROP_PRICE , 1 , price ) ;
//--- A trend line rays to infinity by default - that would paint the whole chart.
ObjectSetInteger ( 0 , name , OBJPROP_RAY_LEFT , false ) ;
ObjectSetInteger ( 0 , name , OBJPROP_RAY_RIGHT , false ) ;
ObjectSetInteger ( 0 , name , OBJPROP_COLOR , isBuy ? WARRIOR_SIG_BUY_COLOR : WARRIOR_SIG_SELL_COLOR ) ;
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>
2026-08-19 15:40:52 -04:00
//--- Thicker on both layers for the same reason the span grew (2026-08-19): a 1px dotted dark
//--- line on a candle chart is invisible at any realistic zoom. The trade layer stays the
//--- heavier of the two so the ranking still reads at a glance.
ObjectSetInteger ( 0 , name , OBJPROP_WIDTH , isTrade ? 3 : 2 ) ;
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>
2026-08-19 14:11:49 -04:00
ObjectSetInteger ( 0 , name , OBJPROP_STYLE , isTrade ? STYLE_SOLID : STYLE_DOT ) ;
//--- Not selectable: these are readouts, and a chart carrying thousands of them becomes
//--- unusable if a stray drag can pick one up and move it.
ObjectSetInteger ( 0 , name , OBJPROP_SELECTABLE , false ) ;
ObjectSetInteger ( 0 , name , OBJPROP_HIDDEN , true ) ;
ObjectSetInteger ( 0 , name , OBJPROP_BACK , ! isTrade ) ; // opinions behind the candles, trades in front
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>
2026-08-18 15:41:28 -04:00
ObjectSetInteger ( 0 , name , OBJPROP_TIMEFRAMES , g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS ) ;
ObjectSetString ( 0 , name , OBJPROP_TOOLTIP , tooltip ) ;
2026-08-22 00:25:52 -04:00
//--- THE FINDER HALF. Anchored to the candle's extreme rather than the trigger price so it
//--- clears the body at every zoom - the whole point is to be visible when the line is not.
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>
2026-08-20 13:50:33 -04:00
string an = WarriorSignalArrowName ( name ) ;
int shift = iBarShift ( _Symbol , period , t , true ) ;
double anchorPrice = price ;
if ( shift > = 0 )
anchorPrice = isBuy ? iLow ( _Symbol , period , shift ) : iHigh ( _Symbol , period , shift ) ;
if ( ! MathIsValidNumber ( anchorPrice ) | | anchorPrice < = 0.0 )
anchorPrice = price ;
ObjectCreate ( 0 , an , OBJ_ARROW , 0 , t , anchorPrice ) ;
ObjectSetInteger ( 0 , an , OBJPROP_TIME , 0 , t ) ;
ObjectSetDouble ( 0 , an , OBJPROP_PRICE , 0 , anchorPrice ) ;
ObjectSetInteger ( 0 , an , OBJPROP_ARROWCODE , isBuy ? WARRIOR_SIG_CODE_BUY : WARRIOR_SIG_CODE_SELL ) ;
//--- ANCHOR is what keeps the glyph OUTSIDE the candle: its top pinned to the low hangs it below,
//--- its bottom pinned to the high stands it above. Anchoring the centre would bury it in the wick.
ObjectSetInteger ( 0 , an , OBJPROP_ANCHOR , isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM ) ;
ObjectSetInteger ( 0 , an , OBJPROP_COLOR , isBuy ? WARRIOR_SIG_BUY_COLOR : WARRIOR_SIG_SELL_COLOR ) ;
ObjectSetInteger ( 0 , an , OBJPROP_WIDTH , isTrade ? 2 : 1 ) ;
ObjectSetInteger ( 0 , an , OBJPROP_SELECTABLE , false ) ;
ObjectSetInteger ( 0 , an , OBJPROP_HIDDEN , true ) ;
ObjectSetInteger ( 0 , an , OBJPROP_BACK , ! isTrade ) ;
ObjectSetInteger ( 0 , an , OBJPROP_TIMEFRAMES , g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS ) ;
ObjectSetString ( 0 , an , OBJPROP_TOOLTIP , tooltip ) ;
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>
2026-08-18 15:41:28 -04:00
}
feat(panel): one live vote line, no stale era count, no per-model HUD
Two chart-display fixes reported after watching a converged 4-model
ensemble: the ensemble panel's trailing "(era 69, 4 models,
DEPLOYING)" was frozen at whatever era the ensemble happened to
deploy on, and the separate top-right HUD (one line per model, raw
B/S/N + weight + era + error) was clutter once the vote itself is
what matters.
Root cause of the freeze: g_ensembleVoteLine is written once per era,
at pass-3 completion. A deployed/converged ensemble runs no further
eras (ScheduleTrainingIfNeeded's trainingComplete branch skips
Train() entirely), so that line could never update again - the era
count and "DEPLOYING" marker were permanent set-dressing from the
deploying era, not a live reading.
- EnsembleScoreCombinedVote() drops the era/DEPLOYING tail once
g_ensDeployApproved - nothing left there worth freezing.
- UpdateVoteReadout() (the aggregate "VOTE ..." line, previously its
own top-right chart object) now writes g_liveVoteLine instead of
drawing anything. Both status-label builders - PublishEnsembleStatus
for the ensemble panel, PublishStatus's choke point for the solo
panel - append it as one line, refreshed every tick/timer exactly
as the old HUD was, so the live vote replaces the frozen era tail
in the same visual slot.
- RefreshVoteReadout()'s per-member loop (DisplayHudLine, one
ObjectLabel per model) is deleted outright rather than folded in -
the operator asked for the aggregate only, "without telling me each
individual network".
Follow-on dead-code removal, since DisplayHudLine was the only
caller: the DispProb/DispSignal/MetaGateArmedNow/MetaHasScore/
MetaLastP/MetaLastBe/MetaApproved/MetaVetoed leg of IChartView (and
its AIBaseChartView/AIBaseChartViewImpl/ExpertSignalAIBase forwards)
had no other reader. The underlying data survives untouched -
m_metaTelemetry is still populated live by SignalMETA.mqh,
m_dispSignal still feeds ProspectiveVote - only the chart-view
forwarding that existed solely to reach the deleted HUD is gone.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:27:41 -04:00
//--- THE VOTE READOUT's own object namespace - now unused (the readout moved into the status
//--- label, see g_liveVoteLine), kept because WarriorChartPrefixes() still purges it defensively
//--- for a chart that has an old build's objects on it.
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>
2026-08-18 18:22:45 -04:00
# define VOTE_HUD_PREFIX " WarriorVoteHUD "
feat(panel): one live vote line, no stale era count, no per-model HUD
Two chart-display fixes reported after watching a converged 4-model
ensemble: the ensemble panel's trailing "(era 69, 4 models,
DEPLOYING)" was frozen at whatever era the ensemble happened to
deploy on, and the separate top-right HUD (one line per model, raw
B/S/N + weight + era + error) was clutter once the vote itself is
what matters.
Root cause of the freeze: g_ensembleVoteLine is written once per era,
at pass-3 completion. A deployed/converged ensemble runs no further
eras (ScheduleTrainingIfNeeded's trainingComplete branch skips
Train() entirely), so that line could never update again - the era
count and "DEPLOYING" marker were permanent set-dressing from the
deploying era, not a live reading.
- EnsembleScoreCombinedVote() drops the era/DEPLOYING tail once
g_ensDeployApproved - nothing left there worth freezing.
- UpdateVoteReadout() (the aggregate "VOTE ..." line, previously its
own top-right chart object) now writes g_liveVoteLine instead of
drawing anything. Both status-label builders - PublishEnsembleStatus
for the ensemble panel, PublishStatus's choke point for the solo
panel - append it as one line, refreshed every tick/timer exactly
as the old HUD was, so the live vote replaces the frozen era tail
in the same visual slot.
- RefreshVoteReadout()'s per-member loop (DisplayHudLine, one
ObjectLabel per model) is deleted outright rather than folded in -
the operator asked for the aggregate only, "without telling me each
individual network".
Follow-on dead-code removal, since DisplayHudLine was the only
caller: the DispProb/DispSignal/MetaGateArmedNow/MetaHasScore/
MetaLastP/MetaLastBe/MetaApproved/MetaVetoed leg of IChartView (and
its AIBaseChartView/AIBaseChartViewImpl/ExpertSignalAIBase forwards)
had no other reader. The underlying data survives untouched -
m_metaTelemetry is still populated live by SignalMETA.mqh,
m_dispSignal still feeds ProspectiveVote - only the chart-view
forwarding that existed solely to reach the deleted HUD is gone.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:27:41 -04:00
//--- THE LIVE AGGREGATED VOTE, one line, refreshed every tick/timer by UpdateVoteReadout() and
//--- read by both status-label builders (PublishEnsembleStatus, CExpertSignalAIBase::PublishStatus)
//--- so there is exactly one place on the chart showing what the combined vote is doing right now -
//--- no more separate top-right HUD, and no more per-member breakdown next to it (user request
//--- 2026-08-24: "a single line... without telling me each individual network").
string g_liveVoteLine = " " ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//+------------------------------------------------------------------+
//| ONE PREDICATE FOR "ARE THE MODELS ON THIS CHART DEPLOYED?" |
//| |
//| It exists because the panel contradicted itself (user report |
//| 2026-08-25): every member line read "Live - learning from new |
//| bars" while the aggregate line under them read "training, not |
//| tradable yet". Neither was lying - they were answering DIFFERENT |
//| questions. The member word came from m_trainingComplete; the |
//| aggregate word came from whether the number had been produced by |
//| ProspectiveVote() rather than by a real Direction() call, which |
//| happens on any bar where every member abstains and says nothing |
//| whatsoever about training state. |
//| |
//| A readout that can disagree with itself is worse than one that is |
//| wrong, because there is no way to tell which half to believe. So |
//| both now resolve through here: members publish their own state, |
//| and every consumer asks this one question of the board. |
//+------------------------------------------------------------------+
# define WARRIOR_MODEL_SLOTS 8
bool g_warriorModelSlotUsed [ WARRIOR_MODEL_SLOTS ] ;
bool g_warriorModelConverged [ WARRIOR_MODEL_SLOTS ] ;
//--- A member writes ONLY its own slot, never reads another's - same split as the vote board in
//--- Variables\ConfidenceBridge.mqh, and for the same reason (see its header note on last-writer-wins).
void PublishModelConverged ( const int slot , const bool converged )
{
if ( slot < 0 | | slot > = WARRIOR_MODEL_SLOTS )
return ;
g_warriorModelSlotUsed [ slot ] = true ;
g_warriorModelConverged [ slot ] = converged ;
}
//--- How many models have published, and how many of those call themselves converged.
void WarriorModelCensus ( int & published , int & converged )
{
published = 0 ;
converged = 0 ;
for ( int i = 0 ; i < WARRIOR_MODEL_SLOTS ; i + + )
if ( g_warriorModelSlotUsed [ i ] )
{
published + + ;
if ( g_warriorModelConverged [ i ] )
converged + + ;
}
}
//--- DEPLOYED means EVERY published model is converged, not "at least one". A half-trained ensemble
//--- still votes, and its vote is diluted by the members that abstain while they train - calling that
//--- deployed would present a number measured on a different set of models than the one trading.
bool WarriorChartModelsDeployed ( void )
{
int published = 0 , converged = 0 ;
WarriorModelCensus ( published , converged ) ;
return ( published > 0 & & converged = = published ) ;
}
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>
2026-08-19 06:58:03 -04:00
//--- Overlay declustering window, in bars - same default as the per-member arrows'
//--- m_signalClusterWindow. A root-level constant rather than a borrowed member because the root
//--- has no AI state and the two layers may legitimately diverge later.
# define OVERLAY_NMS_WINDOW 6
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- The one resolution point for the Trade direction input. Every gate - live entry,
//--- reconstruction, HUD verdict - resolves through here so they cannot drift apart.
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>
2026-08-19 11:10:05 -04:00
TRADING_DIRECTION WarriorEffectiveDirection ( void )
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
return tradingdirection ;
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>
2026-08-19 11:10:05 -04:00
}
bool WarriorDirectionAllows ( const bool isLong )
{
TRADING_DIRECTION d = WarriorEffectiveDirection ( ) ;
return isLong ? ( d ! = SHORT_ONLY ) : ( d ! = LONG_ONLY ) ;
}
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
//+------------------------------------------------------------------+
//| THE VOTE NORMALIZATION RULE, and the only copy of it. |
//| |
//| Live and replay had one each, and they drifted - the replay used |
//| ModuleWeight() where live used VoteCapableWeight(), so a META |
//| head (a gate, structurally unable to agree) sat in the replay's |
//| divisor shrinking every reconstructed vote. Both now Add() here. |
//| |
//| The rule, in one place: |
//| - CAPABLE weight always enters the divisor, contribution or not. |
//| An abstainer LOOKED and said nothing, and diluting the |
//| consensus is exactly what it should do. |
//| - a member that could not look at all (no era-end snapshot, not |
//| yet trained, a gate) contributes NO capable weight and so is |
//| not in the divisor - the caller simply never Add()s it. |
//| - only a non-zero contribution counts as a VOTER, which is what |
//| the readout's "N voter(s)" means. |
//| |
docs(vote): correct the stdlib rationale - m_weight IS respected
The comment added in 616e071 said the stdlib divisor "is a correct mean
only while m_weight is its default 1.0", which reads as though
CExpertSignal ignores the weight. It does not: each signal weights its
own conditions, m_weight*(LongCondition()-ShortCondition()), and every
child applies its own in turn. The weight is respected end to end.
The one real divergence is the NORMALIZER, and the argument for ours is
not deflation - both forms scale identically with agreement, so stdlib's
is a valid relative consensus measure. It is that stdlib's output scale
IS the mean module weight, and we re-derive that from held-out win rates
every era. Measured on USDJPY across five eras in this morning's log the
mean ran 0.162 -> 0.285, a 76% swing, so under /count every vote would
have risen 76% with no change in agreement or accuracy and a fixed
threshold would mean something different each era. Dividing by capable
weight cancels that factor, which is what makes the number a win rate
the threshold, the deploy gate and break-even can be compared against.
Noted in the same block: stdlib arithmetic would be exactly right with
m_weight left at 1.0 and the win rate carried by the pattern weight.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:53:23 -04:00
//| WHY NOT CExpertSignal::Direction()'s divisor. Not because the |
//| stdlib ignores m_weight - it does not. Each signal weights its |
//| OWN conditions (m_weight*(Long-Short)) and every child applies |
//| its own in turn, so the weight is respected end to end. The ONE |
//| divergence is the normalizer: stdlib divides by the COUNT of |
//| participating filters, we divide by the summed CAPABLE weight. |
//| |
//| Both scale identically with agreement, so stdlib's is a perfectly |
//| valid RELATIVE consensus measure. What it is not is an ABSOLUTE |
//| one: its output scale is the mean module weight, and we re-derive |
//| that from held-out win rates every era. Measured on USDJPY across |
//| five eras, the mean module weight ran 0.162 -> 0.285 - a 76% |
//| swing - so under /count every vote would have risen 76% with no |
//| change in agreement or accuracy, and a fixed threshold would mean |
//| something different each era. Dividing by capable weight cancels |
//| that factor, which is what makes the result a WIN RATE the |
//| threshold, the deploy gate and break-even can all be compared to. |
//| |
//| The stdlib arithmetic would be exactly right with m_weight left |
//| at its 1.0 default and the win rate carried by the PATTERN weight |
//| instead - see project_direction_is_a_transaction. That is a live |
//| semantic change, not a refactor, so it is not done here. |
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
//+------------------------------------------------------------------+
struct SVoteAccumulator
{
double num ; // signed sum of contributions
double capable ; // divisor: total capable weight, abstainers included
int voters ; // members that actually took a side
SVoteAccumulator ( void ) { Reset ( ) ; }
void Reset ( void ) { num = 0.0 ; capable = 0.0 ; voters = 0 ; }
void Add ( const double contribution , const double capableWeight )
{
capable + = capableWeight ;
if ( contribution = = 0.0 )
return ; // abstention: dilutes the consensus, is not a voter
num + = contribution ;
voters + + ;
}
double Net ( void ) const { return ( capable > 0.0 ) ? ( num / capable ) : 0.0 ; }
} ;
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>
2026-08-19 11:34:27 -04:00
//--- The symbol's own trading-session table, asked two questions (2026-08-19 user request:
2026-08-22 00:25:52 -04:00
//--- "everything will be dynamic and self adapting to DST"). Is `now` (server time) inside any
//--- trading session of its weekday?
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>
2026-08-19 11:34:27 -04:00
bool WarriorMarketOpenNow ( const string symbol , const datetime now )
{
MqlDateTime dt ;
TimeToStruct ( now , dt ) ;
int secOfDay = dt . hour * 3600 + dt . min * 60 + dt . sec ;
datetime from = 0 , to = 0 ;
for ( uint s = 0 ; SymbolInfoSessionTrade ( symbol , ( ENUM_DAY_OF_WEEK ) dt . day_of_week , s , from , to ) ; s + + )
{
if ( secOfDay > = ( int ) from & & secOfDay < ( int ) to )
return true ;
}
return false ;
}
//--- The LAST session close of the given weekday, in seconds from that day's midnight (86400 on
//--- symbols that trade to midnight). -1 = no trading that day.
int WarriorMarketCloseSeconds ( const string symbol , const int dayOfWeek )
{
datetime from = 0 , to = 0 ;
int lastTo = -1 ;
for ( uint s = 0 ; SymbolInfoSessionTrade ( symbol , ( ENUM_DAY_OF_WEEK ) dayOfWeek , s , from , to ) ; s + + )
lastTo = ( int ) to ;
return lastTo ;
}
2026-08-09 14:51:59 -04:00
//
2026-08-12 15:20:33 -04:00
# define MAX_TABLE_ROWS 1000 / / default row cap before the oldest entry is pruned ; the live
// value comes from the DB_MaxRowsPerTable input via
// MaxTableRows() - raised for meta-label corpus builds
2026-08-09 14:51:59 -04:00
# define MIN_TRADES_FOR_WIN_RATE 100 / / minimum sample size before a pattern ' s win rate is trusted
# define NO_DATA_WIN_RATE -1 / / sentinel : not enough trades to compute a win rate
//--- Hard floor on SL distance from entry, as an ATR multiple. Pure sanity net: the broker's own
//--- SYMBOL_TRADE_STOPS_LEVEL is enforced separately and precisely by TCAdjustStops() further down.
# define MIN_SL_ATR_MULTIPLIER 0.5
2026-08-22 00:25:52 -04:00
//--- The SL_PREV_SWING / TP_PREV_SWING sentinels (-101) were REMOVED 2026-07-31 along with every
//--- other swing anchor on SL and TP - see STOP_LOSS_MODE in Enumerations\InputEnums.mqh.
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- The SL_INTELLIGENT_MODE / TP_INTELLIGENT_MODE / ENTRY_INTELLIGENT_MODE sentinels and their four
//--- tuning constants went 2026-08-25 with the confidence-scaled trade management. m_sl_mode and
//--- m_tp_mode are now UNCONDITIONALLY literal ATR multiples and m_entry_multiplier an unconditional
//--- signed ATR offset - which is what lets OpenParams() read them straight through with no sentinel
//--- test, and what makes a tester GA sweep of them mean exactly what it appears to mean.
//--- ENTRY_MULTIPLIER's "Prev swing" sentinel (-101), still declared for the commented-out enum member.
2026-08-09 14:51:59 -04:00
# define ENTRY_PREV_SWING_MODE ( -101 )
//
class CExpertSignalCustom : public CExpertSignal
{
private :
void DeleteOldestEntry ( string tableName ) ;
//--- (CheckForDuplicateTrade / FindLastTradeIndex / UpdateTradeStatusAndExit were declared here but
//--- never defined anywhere and never called - removed. Nothing linked against them; they only made
//--- it look as though duplicate-trade detection existed on this class.)
void UpdateTradeRecordInDatabase ( string tableName , TradeRecord & tradeRecord ) ;
void ProcessSignal ( SignalInfo & signal ) ;
void BufferSignal ( SignalInfo & signal ) ;
bool CheckClosePosition ( bool isLong , double & price ) ;
bool CheckOpenPosition ( bool isLong , double & price , double & sl , double & tp , datetime & expiration ) ;
bool ShouldTraceTradeRejections ( void ) const ;
//--- Mirrors CExpertTrade::Buy()/Sell()'s own price-vs-stops-level decision so OpenParams() can
//--- validate the stops against the order type the trade layer is actually going to send.
ENUM_ORDER_TYPE ResolveOrderType ( bool isLong , double price ) ;
fix(build): four compile faults - one was a SILENT enum collision that inverted the direction policy
Reported by the user's MetaEditor compile of f64e0f8 (26 errors, 4 warnings). The four
warnings mattered more than the errors.
1. INTELLIGENT WAS TWO ENUMS. MONEY_MANAGEMENT_STRATEGY::INTELLIGENT (=1) is declared
BEFORE TRADING_DIRECTION::INTELLIGENT (=3) in InputEnums.mqh, so MQL5 resolved every
'tradingdirection == INTELLIGENT' to the MM member and converted it to value 1 =
TRADING_DIRECTION::LONG_ONLY. Wrong in both directions at once: selecting Intelligent
(3) matched NOTHING and silently traded both sides, while selecting Long only (1)
matched and handed the decision to the measured drift verdict - which can answer
SHORT_ONLY, so the one setting that must never go short could have. Reported by the
compiler as a WARNING only, never an error. Renamed to DIRECTION_INTELLIGENT; the
VALUE stays 3, so saved .set files are unaffected. Swept every enum in the repo for
sibling collisions (38 enums, detector validated against the pre-fix source, which it
flags): none remain.
2. g_warriorMetaGate sits above the class it points at - added the forward declaration,
the same pattern g_warriorEnsemble already uses in ExpertSignalAIBase.mqh.
3. The broker-time rename (b63e39f) never reached BufferNewTickSignal's PARAMETER or its
two call sites: the local became brokerTime, the parameter stayed gmtTime, and the
body was rewritten to read brokerTime. All five sites now agree.
4. ConfigureAISignal calls IsMetaTarget() from a free function - moved it to the public
section (identity, not an implementation seam); the other meta seams stay protected.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:16:45 -04:00
void BufferNewTickSignal ( string filterID , string pattern , string bias , const MqlDateTime & brokerTime , double entryPrice , double netVote ) ;
2026-08-09 14:51:59 -04:00
string PatternName ( int patternIndex ) { return " Pattern_ " + IntegerToString ( patternIndex ) ; }
SignalInfo signalBuffer [ ] ;
protected :
2026-08-16 21:08:41 -04:00
//--- protected (not private): CExpertSignalAIBase's pattern-database backfill (Expert\AIBase\
//--- OnlineLearning.mqh) calls both directly, so the training-time path can journal into the exact
//--- same tables/rows the live per-tick path (BufferNewTickSignal above) writes to.
void RegisterSignal ( int year , int month , int day , int DOW , int hour , int minutes , string tableName , string pattern , string direction , double entryPrice , double exitPrice , string result , double netVote ) ;
string PatternTableName ( string filterID , string pattern , string direction ) ;
2026-08-09 14:51:59 -04:00
bool m_prohibition_signal ;
bool m_useDatabase ;
CiATR m_ATR ; // ATR indicator
string m_id ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- m_active_pattern/m_active_direction are the SCRATCH slots the signal classes' Long/Short
//--- ladders write into (last-writer-wins WITHIN one ladder is intended - it is the grading).
2026-08-22 00:25:52 -04:00
//--- The DB journaling reads ONLY the per-side slots.
2026-08-09 14:51:59 -04:00
string m_active_pattern ;
string m_active_direction ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string m_active_pattern_long ; // long ladder's match on the last evaluation, or "NULL"
string m_active_pattern_short ; // short ladder's match on the last evaluation, or "NULL"
2026-08-12 12:02:05 -04:00
//--- This filter's own net vote, LongCondition() - ShortCondition(), in pattern-weight units
2026-08-22 00:25:52 -04:00
//--- before m_weight scaling. Same sign as m_lastFiredDirection; journaled into the netVote
//--- column as DATA, never used as a journaling filter - see the per-side journaling comment in
//--- Direction().
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
double m_lastNetVote ;
2026-08-22 00:25:52 -04:00
//--- The two ladder results behind m_lastNetVote, kept apart from it because the net alone
//--- cannot answer "at what weight".
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>
2026-08-18 15:41:28 -04:00
int m_lastLongWeight ;
int m_lastShortWeight ;
2026-08-22 00:25:52 -04:00
//--- HISTORICAL FILTERED-OVERLAY sweep state (see AdvanceFilteredOverlay).
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
//--- VOTE-LEVEL COOLDOWN. The member-level NMS in CExpertSignalAIBase declusters each MEMBER's
//--- own signal; what trades and what is drawn is the COMBINED VOTE, which had no spacing rule at
//--- all - 2,970 voting bars became 222-273 arrows with nothing between them. This is that rule.
//--- Live state: bar time of the last KEPT vote, plus a cached decision for the bar currently
//--- being evaluated. The cache is load-bearing, not an optimisation: Direction() is a
//--- TRANSACTION that journals, draws and consumes one-shot state, and it can run more than once
//--- on the same bar - without this a second call would flip the bar's own verdict.
datetime m_voteCoolKeptTime ;
datetime m_voteCoolEvalTime ;
bool m_voteCoolEvalAccept ;
2026-08-27 10:34:46 -04:00
//--- Overlay sweep record, filled OLDEST-FIRST: the sweep decrements a SERIES index, so it walks
//--- oldest -> newest (the m_overlayIndex comment used to claim the opposite and was wrong). The
//--- prune reads it FORWARD, so the overlay keeps the FIRST bar of a cluster exactly as the live
//--- path does. It is a second pass and not inline because the sweep is CHUNKED across ticks -
//--- local state would reset at every chunk boundary and declutter nothing.
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
datetime m_overlayVoteTime [ ] ;
bool m_overlayVoteBuy [ ] ;
int m_overlayVoteCount ;
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
bool m_overlayPending ;
2026-08-27 10:34:46 -04:00
int m_overlayIndex ; // next SERIES index to process; DECREMENTS, so oldest -> newest
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 129a0d4 is what made this findable - a blank chart
that cannot say why is indistinguishable from a broken one, and this was the
first thing it caught.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:17:25 -04:00
int m_overlayStopIndex ; // lowest (most recent) series index the sweep reaches
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- Bar time at which the EA took over drawing arrows itself. The sweep RECONSTRUCTS what the
//--- vote would have been; forward of this the arrows are the real decision, placed by
2026-08-22 00:25:52 -04:00
//--- CheckOpenPosition after the order parameters validated.
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
datetime m_overlayLiveCutoff ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
//--- Per-sweep census, so a blank filtered view can state its own cause - see the report at the
//--- end of AdvanceFilteredOverlay().
int m_overlaySweptBars ;
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:58:34 -04:00
//--- Bars where at least ONE member had a snapshot entry, i.e. the divisor was non-zero. Split
//--- from m_overlayVotedBars (which also requires a NON-ZERO net) because those two being one
//--- number made three different blank-chart causes print the same sentence - and it printed
//--- "no member has a completed era yet" while the members were on era 23 (2026-08-24).
//--- hadData == 0 -> nobody has published a snapshot: a member/index/era problem
//--- hadData > 0, voted == 0 -> every member looked and said Neutral: a calibration outcome
//--- voted > 0, drawn == 0 -> the vote was real but never cleared the threshold
int m_overlayHadDataBars ;
2026-08-26 16:53:16 -04:00
//--- Bars that DID carry a member snapshot and still had a zero divisor - i.e. every member that
//--- looked was ruled no-skill (HasDemonstratedEdge). A distinct cause from "nobody published".
int m_overlaySnapNoWeightBars ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
int m_overlayVotedBars ;
int m_overlayDrawn ;
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>
2026-08-19 09:36:15 -04:00
//--- Census-log change latch (2026-08-19): the sweep completes ~once a minute and its census
2026-08-22 00:25:52 -04:00
//--- line printed every time - ~560 near-identical lines/day.
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>
2026-08-19 09:36:15 -04:00
int m_overlayLastLogDrawn ;
double m_overlayLastLogBest ;
int m_overlaySkippedLogs ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
double m_overlayBestNet ;
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
int m_overlayVotedBuy ;
int m_overlayVotedSell ;
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>
2026-08-19 06:58:03 -04:00
//--- Sweep-scoped NMS state (see the decluster block in AdvanceFilteredOverlay). Members rather
//--- than locals because the sweep is chunked across timer slices; reset at every arm.
int m_overlayNmsLastBuyIdx ;
int m_overlayNmsLastSellIdx ;
int m_overlayNmsKeptIdx ;
bool m_overlayNmsKeptBuy ;
double m_overlayNmsKeptNet ;
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
//--- COMBINED-VOTE REPLAY SCORE, tallied by the sweep on every bar whose reconstructed vote
//--- clears the open threshold under the direction policy - the same population the era-end vote
//--- scorer counts. Tallied BEFORE declustering: NMS thins the drawn arrows, not the calls the
//--- vote made. Harvested once per completed sweep (TakeOverlayVoteScore) to backfill an EMPTY
//--- ensemble record - a deployed ensemble runs no further eras, so without this the panel's
//--- "Vote win rate" sat on "measuring..." forever after the replay pass (user report 2026-08-25).
long m_overlayVoteFired ;
long m_overlayVoteWins ;
bool m_overlayScoreReady ;
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>
2026-08-18 18:22:45 -04:00
//--- Session peak |vote|, for the readout. The single most useful number for choosing
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 38a12a2) described as live and
still firing every bar;
- the m_lastNonNeutralSignal alternation gate (removed 2026-08-01)
described as consuming the AI's vote;
- 16 lines of VOTE_CLOSE_PRESETS documentation orphaned by that enum's
deletion, ending with "see that enum's note directly above" pointing
at nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:39:01 -04:00
//--- Signal_ThresholdOpen: a threshold above the peak can never fire, and until this was on screen the
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>
2026-08-18 18:22:45 -04:00
//--- only way to learn that was to wait an era and read the gate line.
double m_votePeak ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
//--- Live voter count from the most recent Direction() call. RefreshVoteReadout() keys on it: a
//--- bar with real voters keeps its display; only a voterless bar is repainted prospectively.
int m_lastLiveVoters ;
2026-08-12 15:20:33 -04:00
int m_maxTableRows ; // per-table row cap, from the DB_MaxRowsPerTable input
2026-08-09 14:51:59 -04:00
int m_pattern_count ;
double m_entry_multiplier ; // Configurable multiple for ATR entry adjustment
int m_periods ; // ATR periods
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
int m_sl_mode ; // STOP_LOSS_MODE int: a literal ATR multiple below/above the entry
int m_tp_mode ; // TAKE_PROFIT_MODE int: a literal ATR multiple from the entry
2026-08-22 00:25:52 -04:00
//--- HOLD-TO-BARRIER exit policy (2026-08-15, fractal-target fidelity). The deploy gate
//--- certifies a win rate measured on hold-to-resolution outcomes: entry at the signal bar, then
//--- the measured SL or TP decides.
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>
2026-08-15 16:18:11 -04:00
bool m_holdToBarrier ;
2026-08-09 14:51:59 -04:00
double m_dbConfidence ; // last average normalized DB win-rate across active filters
2026-08-22 00:25:52 -04:00
//--- Direction()'s per-second aggregation state. The window key is a full timestamp (broker
//--- clock since 2026-08-19), NOT MqlDateTime.sec.
2026-08-09 14:51:59 -04:00
datetime m_directionCurrentSecond ;
double m_directionAggregatedResult ;
int m_directionCount ;
double m_directionLastResult ;
int m_lastFiredDirection ; // +1 Buy / -1 Sell / 0 none - THIS filter's own latest vote,
// set in Direction() before children are added in. Unlike
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
// GetActivePatternLong()/Short(), never consumed/reset by
// a read - a pure peek, safe for a parent to poll every tick.
2026-08-09 14:51:59 -04:00
public :
CExpertSignalCustom ( void ) ;
~ CExpertSignalCustom ( void ) ;
virtual bool AddFilter ( CExpertSignal * filter ) ;
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
//--- THE ADMINISTRATIVE TREE: every child this node owns. Kept as its own pair of accessors
//--- (rather than callers reaching into m_filters) because indicators, ticks, panel commands and
//--- trait counts must reach every child, and a child missing from them silently stops training
//--- or stops answering the panel.
int ChildSignalCount ( void ) const { return m_filters . Total ( ) ; }
CExpertSignalCustom * ChildSignalAt ( const int i ) { return ( CExpertSignalCustom * ) m_filters . At ( i ) ; }
2026-08-09 14:51:59 -04:00
virtual bool CheckOpenLong ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool CheckOpenShort ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool CheckCloseLong ( double & price ) override ;
virtual bool CheckCloseShort ( double & price ) override ;
bool OpenParams ( bool isLong , double & price , double & sl , double & tp , datetime & expiration ) ; // Added for generalized parameter calculation
virtual bool OpenLongParams ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool OpenShortParams ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool ValidationSettings ( void ) override ;
virtual bool InitIndicators ( CIndicators * indicators ) override ;
void Entry_Multiplier ( double entry_multiplier ) { m_entry_multiplier = entry_multiplier ; }
void Periods ( int periods ) { m_periods = periods ; }
void SLMode ( int value ) { m_sl_mode = value ; }
void TPMode ( int value ) { m_tp_mode = value ; }
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>
2026-08-15 16:18:11 -04:00
void HoldToBarrier ( bool value ) { m_holdToBarrier = value ; }
bool HoldToBarrier ( void ) const { return m_holdToBarrier ; }
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
//--- HISTORICAL EVALUATION SHIFT. Non-zero only while HistoricalNetVote() replays a past bar for
//--- the filtered overlay; 0 = normal live behaviour (base rule: every_tick ? 0 : 1).
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>
2026-08-13 16:19:43 -04:00
int m_evalShift ;
void EvalShift ( const int shift ) { m_evalShift = shift ; }
ditch(signals): remove the four classic votes - all 26 patterns measured at chance
research/classic.py transcribed all 26 shipped vote patterns (MA 4, RSI 4, MACD 6,
Ichimoku 12) with their constructor weights and tested them as entries on 178k-bar
histories, four instruments x three barrier geometries. Nothing separated from
chance - not one pattern, not the averaged vote at any threshold 10-70, not a
2/3/4-module quorum, not event-plus-confirmation. Residual E[R] everywhere was
-0.01 to -0.08 R, which is approximately the spread. The +4 sigma reading that had
once justified the set was two bars of lookahead: closing it took MACD_p4 on EURUSD
from +5.05pp to -0.02pp.
All four inputs have shipped false ever since, so this deletes dormant code rather
than changing behaviour.
RETRAIN-NEUTRAL, deliberately. EnableMA and EnableRSI were hashed UNCONDITIONALLY
into the DB config fingerprint, so they become literal 0 legacy slots - the same
treatment the ind_Periods slot two lines above already uses, and every existing
database keeps its key. EnableMACD/EnableIchimoku were appended only when enabled,
so with both gone the segment simply never appears, which is byte-identical to
today. No .nnw or .db is orphaned.
WHAT THIS COSTS, STATED PLAINLY: these four were CSignalMETA's only wired candidate
sources, so the on-chart ladder sweep (BuildCorpusBySweep) now has nothing to sweep
and a META chart is no longer self-contained. That is survivable rather than fatal
because MetaPrepareEra already falls back to CMetaCorpus::LoadLargestOnDisk, and its
own comment names this exact case - "charts whose classic filters are disabled".
Use_MetaLabeling ships false regardless. SignalMETA.mqh is otherwise UNTOUCHED, and
its 26-slot one-hot stays at 26: a tester-built corpus on disk still encodes those
pattern ids, and narrowing the descriptor would invalidate every stored corpus.
Signals/SignalMA.mqh SignalRSI.mqh SignalMACD.mqh SignalIchimoku.mqh deleted
Signals/OscillatorDivergence.mqh deleted - RSI and MACD were its only users
Classic_Shift deleted - the four votes were its only readers
Compile-verified in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline
taken before any edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:08:43 -04:00
//--- CONFIGURED EVALUATION BAR. Left at -1 by every shipped filter since the classic votes were
//--- removed, so StartIndex() falls through to the every_tick base rule; the sweep still wins.
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>
2026-08-19 19:09:58 -04:00
int m_shift ;
void Shift ( const int shift ) { m_shift = ( shift < 0 ? -1 : shift ) ; }
virtual int StartIndex ( void )
{
if ( m_evalShift > 0 )
return m_evalShift ;
return ( m_shift > = 0 ? m_shift : ( m_every_tick ? 0 : 1 ) ) ;
}
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- Signed live confidence: sign gives direction (+ buy, - sell), magnitude is the calibrated
//--- 0..1 conviction. 0.0 = no AI filter / not converged yet. TELEMETRY ONLY - the unsigned
//--- AIConfidence() companion was removed 2026-08-25 when its last consumer (the confidence-scaled
//--- SL/TP/lot modes) went; see Variables\ConfidenceBridge.mqh for why nothing may read this to
//--- size a trade.
2026-08-09 14:51:59 -04:00
virtual double SignedAIConfidence ( void ) { return 0.0 ; }
2026-08-22 00:25:52 -04:00
//--- Returns this instance's own SignedAIConfidence() when it IS an AI signal, otherwise the
//--- live value the AI signal publishes each tick (g_LiveAISignedConfidence, see
//--- CExpertSignalAIBase::ScheduleTrainingIfNeeded).
2026-08-09 14:51:59 -04:00
double LiveSignedConfidence ( void ) ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- Refreshes the two journalled confidence globals for the trade OpenParams() is about to hand
//--- to Money. Returns nothing: it has no callers that act on a value, and keeping it void is what
//--- stops it quietly becoming a control input again.
void PublishConfidenceTelemetry ( void ) ;
2026-08-09 14:51:59 -04:00
virtual void ApplyPatternWeight ( int patternNumber , int weight ) { } ;
void ID ( string id ) { m_id = id ; }
virtual string GetFilterID ( void ) { return m_id ; } ;
2026-08-22 00:25:52 -04:00
//--- Is this filter one of the neural nets? Overridden true by CExpertSignalAIBase. A virtual
//--- rather than a GetFilterID() string comparison because the ids are FOLDER names that outlive
//--- display renames (SignalHYBRID's "ConvLSTM"/"HYB" pair), so a name test would silently start
//--- returning the wrong answer the next time a model is renamed.
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>
2026-08-18 15:41:28 -04:00
virtual bool IsAIFilter ( void ) const { return false ; }
2026-08-22 00:25:52 -04:00
//--- CONTROL-PANEL SEAM. The panel used to drive training through g_aiSignals[] in
//--- Warrior_EA.mq5 - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had
//--- already dropped a member on the floor once (609be10).
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
virtual bool OnSignalCommand ( const ENUM_SIGNAL_COMMAND cmd ) { return false ; }
virtual bool HasSignalTrait ( const ENUM_SIGNAL_TRAIT trait ) { return false ; }
//--- Whole-tree walks: this signal plus every filter, recursively.
int DispatchSignalCommand ( const ENUM_SIGNAL_COMMAND cmd ) ;
int CountSignalTrait ( const ENUM_SIGNAL_TRAIT trait ) ;
feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat 4858507 shipped with: the vote is a confidence percentage,
but only to the extent the pattern weights are measured. AI tier weights sat
at their designed defaults (25/50/75/100) because AI rows only ever arrive
from LIVE journaling, of which a training run produces almost none.
AND A STALE-TIER BUG THAT MADE THE EVIDENCE MEANINGLESS. The OOS scan bucketed
every scanned bar by ConfidenceTier(), which reads dPrevSignal - and
dPrevSignal is assigned in PASS 1 only, never anywhere in the OOS scan. So an
entire era's fires were bucketed by one stale, unrelated bar's confidence and
landed in a SINGLE tier. That is the "tier prec T0:72%(828) T1:n/a(0)
T2:n/a(0) T3:n/a(0)" symptom recorded on 2026-08-16 and attributed to the
calibration clamp. The clamp was real and was fixed then; this is a second,
independent cause of the identical output that survived that fix untouched -
which is why the log kept reading the same afterwards. Two causes, one symptom.
Now ConfidenceTierFor(adjSig): the bar this iteration actually scored.
WHY THIS DOES NOT WRITE ROWS TO THE SIGNAL DB, which was the obvious reading of
"fill the database during training". The user's own observation is the reason:
a classic Pattern_2 is a fixed geometric condition, so its win rate is
legitimately accumulated over years, but an AI Pattern_2 means "confidence
landed in tier 2" and tier 2 under era 100's weights is a different statement
from tier 2 under era 500's. The DB's value is ACCUMULATION, and accumulation
is exactly what is wrong here - it would average together models that no
longer exist, while colliding with the per-table row cap and mixing
measured-on-holdout outcomes into the live ledger's own tables. What the DB
actually supplies is a measured win rate per pattern, and pass 3 already
computes that on held-out bars, thousands at a time. So the model ranks itself
once per era, REPLACING rather than accumulating, which makes the weights
describe the current weights by construction.
ESTIMATOR. Not WinRateFromCounts(): it returns NO_DATA below 100 raw trades
BEFORE shrinking, which here would fire on every tier every era and hand all
four the pooled rate - the tiers could never separate and the mechanism would
be inert. Shrinkage is the answer to a small sample; a floor in front of it
means the shrinkage never runs. Instead: a Beta prior of TIER_PRIOR_EFF_N
pseudo-observations centred on the model's pooled holdout rate, counted in
EFFECTIVE observations, because overlapping triple-barrier labels mean 800 raw
fires can be worth ~12 independent ones. Rounded to the integer, not to the
decade NormalizeWinRate() uses, which would collapse the shrunk tiers back
into one number.
NO SAME-ERA CIRCULARITY, and it falls out of the ordering rather than a guard:
weights are computed at the END of era N, so the vote scored during era N was
cast with era N-1's weights. The deploy gate never grades a vote whose weights
were fitted on the bars it is scoring. Residual leakage remains - the same OOS
bars each era under a different model - and is stated in the code rather than
papered over.
Both DB clobber paths are closed: ApplyPatternWeight() declines once
self-ranked, and UpdateSignalsWeights()' filter.Weight() call is guarded by
SelfRanked() - guarding only the tiers would have let the hourly ranking pass
undo half the self-ranking.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:00:32 -04:00
//--- Does this filter derive its own pattern weights, making the signal DB's ranking
2026-08-22 00:25:52 -04:00
//--- inapplicable to it?
feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat 4858507 shipped with: the vote is a confidence percentage,
but only to the extent the pattern weights are measured. AI tier weights sat
at their designed defaults (25/50/75/100) because AI rows only ever arrive
from LIVE journaling, of which a training run produces almost none.
AND A STALE-TIER BUG THAT MADE THE EVIDENCE MEANINGLESS. The OOS scan bucketed
every scanned bar by ConfidenceTier(), which reads dPrevSignal - and
dPrevSignal is assigned in PASS 1 only, never anywhere in the OOS scan. So an
entire era's fires were bucketed by one stale, unrelated bar's confidence and
landed in a SINGLE tier. That is the "tier prec T0:72%(828) T1:n/a(0)
T2:n/a(0) T3:n/a(0)" symptom recorded on 2026-08-16 and attributed to the
calibration clamp. The clamp was real and was fixed then; this is a second,
independent cause of the identical output that survived that fix untouched -
which is why the log kept reading the same afterwards. Two causes, one symptom.
Now ConfidenceTierFor(adjSig): the bar this iteration actually scored.
WHY THIS DOES NOT WRITE ROWS TO THE SIGNAL DB, which was the obvious reading of
"fill the database during training". The user's own observation is the reason:
a classic Pattern_2 is a fixed geometric condition, so its win rate is
legitimately accumulated over years, but an AI Pattern_2 means "confidence
landed in tier 2" and tier 2 under era 100's weights is a different statement
from tier 2 under era 500's. The DB's value is ACCUMULATION, and accumulation
is exactly what is wrong here - it would average together models that no
longer exist, while colliding with the per-table row cap and mixing
measured-on-holdout outcomes into the live ledger's own tables. What the DB
actually supplies is a measured win rate per pattern, and pass 3 already
computes that on held-out bars, thousands at a time. So the model ranks itself
once per era, REPLACING rather than accumulating, which makes the weights
describe the current weights by construction.
ESTIMATOR. Not WinRateFromCounts(): it returns NO_DATA below 100 raw trades
BEFORE shrinking, which here would fire on every tier every era and hand all
four the pooled rate - the tiers could never separate and the mechanism would
be inert. Shrinkage is the answer to a small sample; a floor in front of it
means the shrinkage never runs. Instead: a Beta prior of TIER_PRIOR_EFF_N
pseudo-observations centred on the model's pooled holdout rate, counted in
EFFECTIVE observations, because overlapping triple-barrier labels mean 800 raw
fires can be worth ~12 independent ones. Rounded to the integer, not to the
decade NormalizeWinRate() uses, which would collapse the shrunk tiers back
into one number.
NO SAME-ERA CIRCULARITY, and it falls out of the ordering rather than a guard:
weights are computed at the END of era N, so the vote scored during era N was
cast with era N-1's weights. The deploy gate never grades a vote whose weights
were fitted on the bars it is scoring. Residual leakage remains - the same OOS
bars each era under a different model - and is stated in the code rather than
papered over.
Both DB clobber paths are closed: ApplyPatternWeight() declines once
self-ranked, and UpdateSignalsWeights()' filter.Weight() call is guarded by
SelfRanked() - guarding only the tiers would have let the hourly ranking pass
undo half the self-ranking.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:00:32 -04:00
virtual bool SelfRanked ( void ) const { return false ; }
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>
2026-08-19 06:58:03 -04:00
//--- The weight this filter contributes to the vote's DENOMINATOR - its say in the consensus -
2026-08-22 00:25:52 -04:00
//--- independent of whether it votes on this particular bar.
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>
2026-08-19 06:58:03 -04:00
virtual double VoteCapableWeight ( void ) { return ( GetPatternCount ( ) > 0 ) ? m_weight : 0.0 ; }
feat(deploy): ship on positive EXPECTANCY, and let the chart draw before convergence
TWO CHANGES, both of which turn a permanent "nothing happens" into a decision.
1. THE DEPLOY GATE ASKS THE WRONG QUESTION. tradeable required the win rate to
clear chance by EDGE_MIN_SIGMAS - "can I PROVE an edge exists" from one OOS
window. On H4 that asks ~66% against a market supplying ~53%, so it is
unreachable by construction and no run has ever deployed through it.
SDeployVerdict now also carries the economics of the geometry actually being
traded - cost-adjusted break-even and reward:risk, both from the new
CostAdjustedGeometry() so a spread convention cannot be applied to one and
missed on the other - and derives
E[R] = (p - p*) * (1 + RR)
which is exactly zero at break-even by construction, so "profitable" and
"beats break-even" can never disagree. Under DeployOnExpectancy (new input,
default ON) tradeable becomes E[R] > 0 and selectionScore ranks eras by
expectancy instead of precision. Coverage and both-sides-live still gate
both: an expectancy over a handful of one-sided calls is not tradeable.
The struct also publishes scoreSE - the SE of selectionScore IN THE SCORE'S
OWN UNITS - because the score changes units with the objective (win-rate
points vs R). Both plateau bands now read it instead of precSE, which was
right for one objective and dimensionally wrong for the other.
Setting DeployOnExpectancy=false restores the previous behaviour exactly.
2. THE FILTERED VIEW COULD NOT DRAW WHILE ANY MODEL WAS TRAINING.
HistoricalNetVote built its divisor from VoteCapableWeight(), which answers
"may this member move real money" and returns 0.0 for an AI member until the
whole run converges. So the reconstruction's divisor was zero on EVERY bar,
every bar was skipped as "nobody looked", and the chart drew nothing at all -
for the entire training run, which before the plateau noise band was forever.
Reported as "no signals drawn since the refactor".
New ReconstructionWeight(): the same weight WITHOUT the converged-run
requirement, overridden on the AI member to ModuleWeight() gated on
SelfRanked() only. The overlay is a picture of what the vote WOULD have
shown, which a mid-training model can answer - the chart HUD already says so
with its "(trn)" marker. Live Direction() still uses VoteCapableWeight(), so
no untrained model gains a say in an order.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:15:06 -04:00
//--- THE RECONSTRUCTION'S divisor share, which is NOT the live one.
//---
//--- VoteCapableWeight() answers "may this member's vote move real money", and for an AI member
//--- that is 0 until the whole training run converges. HistoricalNetVote used it as the divisor,
//--- so while any model was still training the reconstruction's divisor was zero on EVERY bar,
//--- every bar was skipped as "nobody looked", and the filtered view drew nothing at all. Since a
//--- run can train for days - and, before the plateau noise band, effectively forever - that is a
//--- chart that is blank for its entire useful life. Reported 2026-08-24 as "no signals drawn
//--- since the refactor".
//---
//--- The overlay is a picture of what the vote WOULD have shown, which is a question a
//--- mid-training model can answer; the chart HUD already says so with its "(trn)" marker. Live
//--- Direction() keeps VoteCapableWeight() untouched, so no untrained model gains a say in an
//--- order because of this.
virtual double ReconstructionWeight ( void ) { return VoteCapableWeight ( ) ; }
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- AI filters only: this model's cached decision for bar `idx`, already converted to the signed
//--- vote it would have cast. False when the bar was never scored (outside the scan, or a feature
//--- window failure), which is NOT the same as an abstention and must not be counted as one.
virtual bool CachedVoteAt ( const int idx , double & signedVote ) { signedVote = 0.0 ; return false ; }
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
//--- Same question asked of the member's ERA-END SNAPSHOT instead of its live cache. The live
//--- cache is wiped to sentinel at every era start, so anything reading it is blind for most of
//--- every era - the snapshot is copied at pass-3 completion and survives until the next one.
virtual bool SnapshotVoteAt ( const int idx , double & signedVote ) { signedVote = 0.0 ; return false ; }
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
//--- AI filters only: the RESOLVED swing-pivot label at bar `idx` - the truth a reconstructed
//--- vote is scored against. False while the pivot pair is uncommitted (no label exists yet).
//--- The label is a pure function of the shared chart series, so any one AI member's answer
//--- serves the whole vote - see OverlayTruthAt().
virtual bool ReplayTruthAt ( const int idx , ENUM_SIGNAL & truth ) { truth = Neutral ; return false ; }
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 155f56e is live and working:
"Filtered view: swept 4999 bar(s), 767 had a voter, drew 0 arrow(s).
Strongest vote 36.0% against a 40.0% threshold."
4,999 bars against the previous 0. The remaining emptiness is the models, not
the plumbing - see the reply for why lowering the threshold further is the
wrong response to it.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:29:27 -04:00
//--- What this filter WOULD vote right now if it were allowed to - i.e. its current decision put
//--- through the same tier/weight arithmetic, but WITHOUT the readiness gate that stops a model
2026-08-22 00:25:52 -04:00
//--- voting before it is deployed.
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 155f56e is live and working:
"Filtered view: swept 4999 bar(s), 767 had a voter, drew 0 arrow(s).
Strongest vote 36.0% against a 40.0% threshold."
4,999 bars against the previous 0. The remaining emptiness is the models, not
the plumbing - see the reply for why lowering the threshold further is the
wrong response to it.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:29:27 -04:00
virtual bool ProspectiveVote ( double & signedVote , double & weight )
{ signedVote = 0.0 ; weight = 0.0 ; return false ; }
2026-08-22 00:25:52 -04:00
//--- Snapshot/restore of everything a Direction() call writes that a LATER call reads. That is a
//--- corrupted row in the very table the pattern win rates (and now the vote weights) are
//--- computed from.
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
void SaveVoteState ( string & pl , string & ps , double & nv , int & lw , int & sw , int & fd )
{
pl = m_active_pattern_long ; ps = m_active_pattern_short ; nv = m_lastNetVote ;
lw = m_lastLongWeight ; sw = m_lastShortWeight ; fd = m_lastFiredDirection ;
}
void RestoreVoteState ( const string pl , const string ps , const double nv ,
const int lw , const int sw , const int fd )
{
m_active_pattern_long = pl ; m_active_pattern_short = ps ; m_lastNetVote = nv ;
m_lastLongWeight = lw ; m_lastShortWeight = sw ; m_lastFiredDirection = fd ;
}
//--- Chunked historical rebuild of the FILTERED view - see the definition for the whole rationale.
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>
2026-08-23 09:06:46 -04:00
//--- The replay half of Direction(), with live's divisor rule. See the definition for why the
//--- two cannot simply be one call today.
2026-08-26 16:53:16 -04:00
double HistoricalNetVote ( const int idx , double & capableOut , bool & hadSnapshotOut ) ;
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
bool AdvanceFilteredOverlay ( const int barBudget ) ;
void StartFilteredOverlay ( void ) ;
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
//--- VOTE-LEVEL COOLDOWN, live side. Idempotent per bar - see m_voteCoolEvalTime for why that is
//--- required rather than merely tidy. Returns true (accept) when the cooldown is off.
bool VoteCooldownAccept ( const datetime barTime )
{
int window = WarriorSignalCooldownBars ( ) ;
if ( window < = 0 | | barTime < = 0 )
return true ;
if ( m_voteCoolEvalTime = = barTime )
return m_voteCoolEvalAccept ;
long minGap = ( long ) window * PeriodSeconds ( ) ;
bool accept = ! ( m_voteCoolKeptTime ! = 0 & & ( long ) ( barTime - m_voteCoolKeptTime ) < = minGap ) ;
m_voteCoolEvalTime = barTime ;
m_voteCoolEvalAccept = accept ;
if ( accept )
m_voteCoolKeptTime = barTime ;
return accept ;
}
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
bool FilteredOverlayPending ( void ) const { return m_overlayPending ; }
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
//--- The sweep's truth source: the first AI filter's resolved label for the bar - see
//--- ReplayTruthAt() above.
bool OverlayTruthAt ( const int idx , ENUM_SIGNAL & truth ) ;
//--- One-shot harvest of the completed sweep's combined-vote score (fired calls / correct calls).
//--- Consuming read: returns true exactly once per completed sweep, so the caller can never
//--- double-count one sweep into the ensemble record.
bool TakeOverlayVoteScore ( long & fired , long & wins ) ;
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 38a12a2) described as live and
still firing every bar;
- the m_lastNonNeutralSignal alternation gate (removed 2026-08-01)
described as consuming the AI's vote;
- 16 lines of VOTE_CLOSE_PRESETS documentation orphaned by that enum's
deletion, ending with "see that enum's note directly above" pointing
at nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:39:01 -04:00
//--- One-line on-chart readout of the vote that is actually being tested against Signal_ThresholdOpen.
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>
2026-08-18 21:18:55 -04:00
void UpdateVoteReadout ( const double vote , const int voters , const int neutrals , const bool prospective ) ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
//--- Timer-driven repaint of the readout - see the definition for the cadence bug it fixes.
void RefreshVoteReadout ( void ) ;
2026-08-22 00:25:52 -04:00
//--- THIS filter's own arrow namespace.
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>
2026-08-18 15:41:28 -04:00
string FilterArrowPrefix ( void ) { return SIG_ARROW_PREFIX + m_id + " _ " ; }
//--- RAW VIEW: draw this filter's own vote at bar `idx`, named and tooltipped so it identifies
2026-08-22 00:25:52 -04:00
//--- itself on a chart carrying several.
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>
2026-08-18 15:41:28 -04:00
void DrawRawFilterArrow ( const int idx , const string pattern , const bool isBuy ,
const int weight )
{
datetime t = iTime ( m_symbol . Name ( ) , m_period , idx ) ;
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>
2026-08-19 14:11:49 -04:00
//--- THE TRIGGER PRICE: this bar's close, which is where a market order fires and exactly the
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- entry the swing label assumes. The spread the label charges is smaller than a chart pixel
//--- at normal zoom, so it is priced but not drawn.
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>
2026-08-19 14:11:49 -04:00
double price = iClose ( m_symbol . Name ( ) , m_period , idx ) ;
WarriorPlotSignalLevel ( FilterArrowPrefix ( ) + TimeToString ( t ) , t , ( ENUM_TIMEFRAMES ) m_period , price ,
isBuy , false ,
StringFormat ( " %s %s %s (weight %d, module %.2f) " , m_id , ( isBuy ? " Buy " : " Sell " ) ,
pattern , weight , m_weight ) ) ;
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>
2026-08-18 15:41:28 -04:00
}
//--- Remove this filter's arrow at bar `idx` - the counterpart to the draw above, for a bar whose
//--- vote was withdrawn (a rejected setup, or a redraw that no longer fires there).
void EraseRawFilterArrow ( const int idx )
{
datetime t = iTime ( m_symbol . Name ( ) , m_period , idx ) ;
if ( t > 0 )
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>
2026-08-20 13:50:33 -04:00
WarriorDeleteSignalMark ( FilterArrowPrefix ( ) + TimeToString ( t ) ) ;
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>
2026-08-18 15:41:28 -04:00
}
//--- FILTERED VIEW: the combined vote, drawn by the AGGREGATE signal and belonging to no filter.
//--- Bigger and in its own colours precisely so it does not read as "one more model's opinion" -
//--- it is a different kind of statement from the raw arrows and the two must never be confused
//--- on a chart that shows either.
void DrawVoteArrow ( const int idx , const bool isBuy , const double vote ,
const double sl , const double tp )
{
datetime t = iTime ( m_symbol . Name ( ) , m_period , idx ) ;
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>
2026-08-19 14:11:49 -04:00
//--- The trigger price - see DrawRawFilterArrow's note. This is the level the order goes on at.
double price = iClose ( m_symbol . Name ( ) , m_period , idx ) ;
WarriorPlotSignalLevel ( SIG_VOTE_PREFIX + TimeToString ( t ) , t , ( ENUM_TIMEFRAMES ) m_period , price ,
isBuy , true ,
StringFormat ( " TRADE %s @ %s | vote %.1f >= %.1f | SL %s TP %s " ,
( isBuy ? " BUY " : " SELL " ) ,
DoubleToString ( price , m_symbol . Digits ( ) ) , vote , m_threshold_open ,
DoubleToString ( sl , m_symbol . Digits ( ) ) ,
DoubleToString ( tp , m_symbol . Digits ( ) ) ) ) ;
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>
2026-08-18 15:41:28 -04:00
}
void EraseVoteArrow ( const int idx )
{
datetime t = iTime ( m_symbol . Name ( ) , m_period , idx ) ;
if ( t > 0 )
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>
2026-08-20 13:50:33 -04:00
WarriorDeleteSignalMark ( SIG_VOTE_PREFIX + TimeToString ( t ) ) ;
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>
2026-08-18 15:41:28 -04:00
}
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- Consuming reads (reset to "NULL" on read), one slot per side - the single-label
//--- GetActivePattern()/GetActiveDirection() pair they replace let the later-running short ladder
//--- steal the long ladder's label (see Direction()'s per-side journaling comment).
string GetActivePatternLong ( void ) ;
string GetActivePatternShort ( void ) ;
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>
2026-08-18 15:41:28 -04:00
//--- NON-consuming peeks at the same two slots. Same relationship to GetActivePattern*() as
2026-08-22 00:25:52 -04:00
//--- m_lastFiredDirection has to those: a pure look, safe to call without stealing the value
//--- from the journaling path that must still receive it.
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>
2026-08-18 15:41:28 -04:00
string PeekActivePatternLong ( void ) { return m_active_pattern_long ; }
string PeekActivePatternShort ( void ) { return m_active_pattern_short ; }
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
double LastNetVote ( void ) { return m_lastNetVote ; }
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>
2026-08-18 15:41:28 -04:00
int LastLongWeight ( void ) { return m_lastLongWeight ; }
int LastShortWeight ( void ) { return m_lastShortWeight ; }
2026-08-22 00:25:52 -04:00
//--- Read access to CExpertSignal's m_weight, which the standard library exposes only as a
//--- SETTER. Named ModuleWeight() rather than Weight() so it cannot be mistaken for (or
//--- accidentally overload) the library's setter.
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>
2026-08-18 15:52:08 -04:00
double ModuleWeight ( void ) const { return m_weight ; }
2026-08-09 14:51:59 -04:00
virtual int GetPatternCount ( void ) { return m_pattern_count ; } ;
2026-08-23 10:16:30 -04:00
//--- Direction()'s two side effects, named so the transaction it performs is visible at the
//--- call site instead of buried in the loop that also does the arithmetic.
void JournalFilterPatterns ( CExpertSignalCustom * filter , const MqlDateTime & brokerTime ) ;
void DrawFilterRawView ( CExpertSignalCustom * filter ) ;
2026-08-09 14:51:59 -04:00
virtual double Direction ( void ) override ;
2026-08-22 00:25:52 -04:00
//--- Vote lifecycle hooks, for filters whose LongCondition()/ShortCondition() consume one-shot
//--- state when they fire. Base = no-op.
2026-08-09 14:51:59 -04:00
virtual void BeginVote ( void ) { }
virtual void RevokeVote ( void ) { }
bool UpdateSignalsWeights ( void ) ;
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>
2026-08-16 21:49:52 -04:00
//--- priorWeight 0 = raw maximum-likelihood ratio (the pre-2026-08-16 behaviour); >0 shrinks the
//--- estimate toward priorPct by that many pseudo-trades. See the definition for why.
int WinRateFromCounts ( const int wins , const int losses , const double priorPct = -1.0 ,
const int priorWeight = 0 ) ;
2026-08-09 14:51:59 -04:00
int NormalizeWinRate ( double winRate ) ;
void ProcessBufferedSignals ( void ) ;
bool InRange ( double value , double min , double max ) ; // Helper function for range checking
void UseDatabase ( bool value ) { m_useDatabase = value ; } ;
2026-08-12 15:20:33 -04:00
void MaxTableRows ( int value ) { m_maxTableRows = MathMax ( 1 , value ) ; } ;
2026-08-09 14:51:59 -04:00
//--- event handler
virtual void OnTickHandler ( void ) ;
virtual void OnChartEventHandler ( const int id ,
const long & lparam ,
const double & dparam ,
const string & sparam ) ;
} ;
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CExpertSignalCustom : : CExpertSignalCustom ( void ) :
m_id ( " NULL " ) ,
m_active_pattern ( " NULL " ) ,
m_active_direction ( " NULL " ) ,
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
m_active_pattern_long ( " NULL " ) ,
m_active_pattern_short ( " NULL " ) ,
m_lastNetVote ( 0.0 ) ,
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>
2026-08-18 15:41:28 -04:00
m_lastLongWeight ( 0 ) ,
m_lastShortWeight ( 0 ) ,
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
m_voteCoolKeptTime ( 0 ) ,
m_voteCoolEvalTime ( 0 ) ,
m_voteCoolEvalAccept ( true ) ,
m_overlayVoteCount ( 0 ) ,
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
m_overlayPending ( false ) ,
m_overlayIndex ( 0 ) ,
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 129a0d4 is what made this findable - a blank chart
that cannot say why is indistinguishable from a broken one, and this was the
first thing it caught.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:17:25 -04:00
m_overlayStopIndex ( 0 ) ,
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
m_overlayLiveCutoff ( 0 ) ,
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
m_overlaySweptBars ( 0 ) ,
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:58:34 -04:00
m_overlayHadDataBars ( 0 ) ,
2026-08-26 16:53:16 -04:00
m_overlaySnapNoWeightBars ( 0 ) ,
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
m_overlayVotedBars ( 0 ) ,
m_overlayDrawn ( 0 ) ,
m_overlayBestNet ( 0.0 ) ,
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>
2026-08-19 09:36:15 -04:00
m_overlayLastLogDrawn ( -1 ) ,
m_overlayLastLogBest ( 0.0 ) ,
m_overlaySkippedLogs ( 0 ) ,
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
m_overlayVotedBuy ( 0 ) ,
m_overlayVotedSell ( 0 ) ,
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>
2026-08-19 06:58:03 -04:00
m_overlayNmsLastBuyIdx ( -1 ) ,
m_overlayNmsLastSellIdx ( -1 ) ,
m_overlayNmsKeptIdx ( -1 ) ,
m_overlayNmsKeptBuy ( false ) ,
m_overlayNmsKeptNet ( 0.0 ) ,
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
m_overlayVoteFired ( 0 ) ,
m_overlayVoteWins ( 0 ) ,
m_overlayScoreReady ( false ) ,
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>
2026-08-18 18:22:45 -04:00
m_votePeak ( 0.0 ) ,
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
m_lastLiveVoters ( 0 ) ,
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>
2026-08-13 16:19:43 -04:00
m_evalShift ( 0 ) ,
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>
2026-08-19 19:09:58 -04:00
m_shift ( -1 ) ,
2026-08-12 15:20:33 -04:00
m_maxTableRows ( MAX_TABLE_ROWS ) ,
2026-08-09 14:51:59 -04:00
m_pattern_count ( 0 ) ,
m_entry_multiplier ( 0 ) ,
m_prohibition_signal ( false ) ,
m_periods ( 14 ) ,
m_useDatabase ( false ) ,
m_sl_mode ( 3 ) , // SL_ATR_x3
m_tp_mode ( 6 ) , // TP_ATR_x6
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>
2026-08-15 16:18:11 -04:00
m_holdToBarrier ( false ) ,
2026-08-09 14:51:59 -04:00
m_dbConfidence ( 0.0 ) ,
m_directionCurrentSecond ( 0 ) ,
m_directionAggregatedResult ( 0.0 ) ,
m_directionCount ( 0 ) ,
m_directionLastResult ( 0.0 ) ,
m_lastFiredDirection ( 0 )
{
}
//+------------------------------------------------------------------+
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//| Live signed confidence: own reading, else the ensemble's mean |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
double CExpertSignalCustom : : LiveSignedConfidence ( void )
{
double own = SignedAIConfidence ( ) ;
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 778b6c0.
1. VOTE COMBINING BELONGS TO THE ORCHESTRATOR, NOT TO A MEMBER.
778b6c0 fixed the last-writer-wins bug on g_LiveAISignedConfidence by having a
member average its siblings through g_warriorEnsemble. That trades a scheduling bug
for a coupling bug, and it is the wrong shape for this EA: every signal runs in its
own instance, minds its own state, and VOTES to the orchestrator, which is the only
thing allowed to combine opinions.
Replaced with a publish/aggregate pair in Variables\ConfidenceBridge.mqh whose split
is enforced by shape rather than by convention:
- PublishAIVote(slot, conf) - a member writes ONLY its own slot, reads nobody's;
- AggregateAIVotes() - called by CExpertSignalCustom::LiveSignedConfidence.
CExpertSignalAIBase::EnsembleLiveSignedConfidence is gone. The orchestrator also
republishes the aggregate into g_LiveAISignedConfidence, because the intelligent
trailing reads that global directly and must act on the same number the exit route
does rather than on a leftover from whichever member ticked last. A solo AI signal
owns slot 0, so the non-ensemble path is unchanged.
2. THE GATE NOW REPLAYS THE REAL EXIT RULE.
SimulateTradeOutcome() walks the same price series with the same fill/barrier/spread
convention as ComputeLabelForBar - deliberately by copy, so a disagreement between
the two can only be a policy effect and never a discrepancy between two pieces of our
own arithmetic - and terminates at the FIRST of stop / target / vote reversal /
horizon. Barriers are tested before the vote on the same bar: intrabar we cannot know
which came first, and the barrier is what the broker executes automatically, so
checking the vote first would credit the exit policy with escapes a real stop would
have taken out of its hands.
It runs AFTER pass 3, not inside it. A vote-flip exit for a trade entered at bar r is
decided by the model's output at bars r-1, r-2, ... - NEWER bars - and pass 3 walks
oldest-to-newest, so at the moment r is graded its own exit does not exist yet. Only
once m_oosDecisionSeries is complete over the whole OOS window can the replay run.
In ensemble mode that series carries the member's adjusted decision and the live exit
reads the ensemble aggregate, which is the coupling the user identified: an LSTM entry
really can be closed by the ensemble turning against it.
3. THE STATISTIC HAS TO CHANGE WITH THE POLICY, AND THAT IS THE REAL FINDING.
A barrier exit pays a fixed R. A vote exit pays whatever the close happens to be. So
the moment vote exits are enabled the payoff is CONTINUOUS, and "win rate vs
break-even" stops being a meaningful test - there is no fixed break-even for a
variable payoff. SimulateTradeOutcome therefore returns R rather than a bool, and the
replay reports expectancy in R with its SE taken from the R distribution (overlap-
deflated on the same EffectiveSampleSize doctrine as every other SE here), not from a
binomial.
This is the same class of error as win-based scoring in 2026-08-09: measuring a
variable-payoff process with a fixed-payoff statistic. Naming it now, while vote exits
are still off, is much cheaper than discovering it after they go on.
4. WHY THIS IS SAFE TO SHIP TODAY.
Min_Vote_Close ships Disabled (101, unreachable on both scales it drives) and reaches
the AI signal through the new ExitPolicy() setter as 1.01, which the setter turns into
0.0 = no vote exit. Under that policy every replayed trade resolves at a barrier and
the simulation is arithmetically the same trade the deploy gate already certifies -
they cannot drift. The report says so explicitly, and prints ONCE per run in that
state; when vote exits are on it prints every era, because then the divergence is the
thing to watch. Nothing about today's numbers moves.
The gate switchover is wired but dormant by construction: it becomes exit-aware the
moment the input is enabled, which is exactly what "the certified number is the traded
number" has to mean.
KNOWN LIMIT, stated rather than hidden: only the AI early-exit route is replayed. The
rule-based averaged-vote close (m_threshold_close) depends on every other filter's
live vote, which pass 3 does not reproduce, so a position the classic filters would
have closed is held to its barrier here. The replay therefore holds LONGER than live
and overstates barrier-reached outcomes. Faithful only while the AI is the dominant
vote - which is the configuration this is being built for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:25:57 -04:00
if ( own ! = 0.0 )
return own ;
2026-08-22 00:25:52 -04:00
//--- THE ORCHESTRATOR COMBINES; the members only publish.
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 778b6c0.
1. VOTE COMBINING BELONGS TO THE ORCHESTRATOR, NOT TO A MEMBER.
778b6c0 fixed the last-writer-wins bug on g_LiveAISignedConfidence by having a
member average its siblings through g_warriorEnsemble. That trades a scheduling bug
for a coupling bug, and it is the wrong shape for this EA: every signal runs in its
own instance, minds its own state, and VOTES to the orchestrator, which is the only
thing allowed to combine opinions.
Replaced with a publish/aggregate pair in Variables\ConfidenceBridge.mqh whose split
is enforced by shape rather than by convention:
- PublishAIVote(slot, conf) - a member writes ONLY its own slot, reads nobody's;
- AggregateAIVotes() - called by CExpertSignalCustom::LiveSignedConfidence.
CExpertSignalAIBase::EnsembleLiveSignedConfidence is gone. The orchestrator also
republishes the aggregate into g_LiveAISignedConfidence, because the intelligent
trailing reads that global directly and must act on the same number the exit route
does rather than on a leftover from whichever member ticked last. A solo AI signal
owns slot 0, so the non-ensemble path is unchanged.
2. THE GATE NOW REPLAYS THE REAL EXIT RULE.
SimulateTradeOutcome() walks the same price series with the same fill/barrier/spread
convention as ComputeLabelForBar - deliberately by copy, so a disagreement between
the two can only be a policy effect and never a discrepancy between two pieces of our
own arithmetic - and terminates at the FIRST of stop / target / vote reversal /
horizon. Barriers are tested before the vote on the same bar: intrabar we cannot know
which came first, and the barrier is what the broker executes automatically, so
checking the vote first would credit the exit policy with escapes a real stop would
have taken out of its hands.
It runs AFTER pass 3, not inside it. A vote-flip exit for a trade entered at bar r is
decided by the model's output at bars r-1, r-2, ... - NEWER bars - and pass 3 walks
oldest-to-newest, so at the moment r is graded its own exit does not exist yet. Only
once m_oosDecisionSeries is complete over the whole OOS window can the replay run.
In ensemble mode that series carries the member's adjusted decision and the live exit
reads the ensemble aggregate, which is the coupling the user identified: an LSTM entry
really can be closed by the ensemble turning against it.
3. THE STATISTIC HAS TO CHANGE WITH THE POLICY, AND THAT IS THE REAL FINDING.
A barrier exit pays a fixed R. A vote exit pays whatever the close happens to be. So
the moment vote exits are enabled the payoff is CONTINUOUS, and "win rate vs
break-even" stops being a meaningful test - there is no fixed break-even for a
variable payoff. SimulateTradeOutcome therefore returns R rather than a bool, and the
replay reports expectancy in R with its SE taken from the R distribution (overlap-
deflated on the same EffectiveSampleSize doctrine as every other SE here), not from a
binomial.
This is the same class of error as win-based scoring in 2026-08-09: measuring a
variable-payoff process with a fixed-payoff statistic. Naming it now, while vote exits
are still off, is much cheaper than discovering it after they go on.
4. WHY THIS IS SAFE TO SHIP TODAY.
Min_Vote_Close ships Disabled (101, unreachable on both scales it drives) and reaches
the AI signal through the new ExitPolicy() setter as 1.01, which the setter turns into
0.0 = no vote exit. Under that policy every replayed trade resolves at a barrier and
the simulation is arithmetically the same trade the deploy gate already certifies -
they cannot drift. The report says so explicitly, and prints ONCE per run in that
state; when vote exits are on it prints every era, because then the divergence is the
thing to watch. Nothing about today's numbers moves.
The gate switchover is wired but dormant by construction: it becomes exit-aware the
moment the input is enabled, which is exactly what "the certified number is the traded
number" has to mean.
KNOWN LIMIT, stated rather than hidden: only the AI early-exit route is replayed. The
rule-based averaged-vote close (m_threshold_close) depends on every other filter's
live vote, which pass 3 does not reproduce, so a position the classic filters would
have closed is held to its barrier here. The replay therefore holds LONGER than live
and overstates barrier-reached outcomes. Faithful only while the AI is the dominant
vote - which is the configuration this is being built for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:25:57 -04:00
g_LiveAISignedConfidence = AggregateAIVotes ( ) ;
return g_LiveAISignedConfidence ;
2026-08-09 14:51:59 -04:00
}
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//+------------------------------------------------------------------+
//| Snapshot both confidence readings for the journal - see the |
//| telemetry rule at the top of Variables\ConfidenceBridge.mqh. |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : PublishConfidenceTelemetry ( void )
2026-08-09 14:51:59 -04:00
{
g_AISignedConfidence = LiveSignedConfidence ( ) ;
g_DBConfidence = m_dbConfidence ;
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CExpertSignalCustom : : ~ CExpertSignalCustom ( void )
{
ArrayFree ( signalBuffer ) ;
}
//+------------------------------------------------------------------+
//| Tester-only trade rejection tracing |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : ShouldTraceTradeRejections ( void ) const
{
return VerboseMode ;
}
void TraceSignalRejection ( const string key , const string message )
{
if ( ! VerboseMode )
return ;
TCLog ( " signal-reject: " + key , message ) ;
}
//+------------------------------------------------------------------+
//| Single source of truth for the per-pattern/direction table name |
//+------------------------------------------------------------------+
string CExpertSignalCustom : : PatternTableName ( string filterID , string pattern , string direction )
{
return filterID + " _ " + pattern + " _ " + direction ;
}
//+------------------------------------------------------------------+
//| Helper function to check value ranges |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : InRange ( double value , double min , double max )
{
return value > = min & & value < = max ;
}
//+------------------------------------------------------------------+
//| Validation settings protected data |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : ValidationSettings ( void )
{
if ( ! CExpertSignal : : ValidationSettings ( ) )
return false ;
// Simplified checks using the InRange helper
if ( ! InRange ( m_periods , 0 , 200 ) )
{
printf ( __FUNCTION__ " : ATR Periods must be 0-200 " ) ;
return false ;
}
if ( ! InRange ( StartIndex ( ) , 0 , 200 ) )
{
printf ( __FUNCTION__ " : ATR shift must be 0-200 " ) ;
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
//| Create indicators |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : InitIndicators ( CIndicators * indicators )
{
//--- check pointer
if ( indicators = = NULL )
return ( false ) ;
//---
CExpertSignal * filter ;
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
//--- WHOLE TREE: a gate is still a signal and needs its indicators and series. It is missing from
//--- CExpertSignal::InitIndicators below (that walks m_filters), which is why it is done here.
int total = ChildSignalCount ( ) ;
2026-08-09 14:51:59 -04:00
//--- gather information about using of timeseries
for ( int i = 0 ; i < total ; i + + )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
filter = ChildSignalAt ( i ) ;
if ( filter = = NULL )
continue ;
2026-08-09 14:51:59 -04:00
m_used_series | = filter . UsedSeries ( ) ;
}
//--- create required timeseries
if ( ! CExpertBase : : InitIndicators ( indicators ) )
return ( false ) ;
//--- initialization of indicators and timeseries in the additional filters
for ( int i = 0 ; i < total ; i + + )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
filter = ChildSignalAt ( i ) ;
if ( filter = = NULL )
continue ;
2026-08-09 14:51:59 -04:00
filter . SetPriceSeries ( m_open , m_high , m_low , m_close ) ;
filter . SetOtherSeries ( m_spread , m_time , m_tick_volume , m_real_volume ) ;
if ( ! filter . InitIndicators ( indicators ) )
return ( false ) ;
}
if ( ! indicators . Add ( GetPointer ( m_ATR ) ) | | ! m_ATR . Create ( m_symbol . Name ( ) , m_period , m_periods ) | | ! CExpertSignal : : InitIndicators ( indicators ) )
{
printf ( __FUNCTION__ " : error initializing indicators " ) ;
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
//| Setting an additional filter |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : AddFilter ( CExpertSignal * filter )
{
if ( filter = = NULL )
return false ;
if ( ! filter .Init ( m_symbol , m_period , m_adjusted_point ) )
return false ;
if ( ! m_filters . Add ( filter ) )
return false ;
filter . EveryTick ( m_every_tick ) ;
filter . Magic ( m_magic ) ;
CExpertSignalCustom * customFilter = dynamic_cast < CExpertSignalCustom * > ( filter ) ;
if ( customFilter ! = NULL )
{
string filterID = customFilter . GetFilterID ( ) ;
if ( filterID ! = " NULL " & & m_useDatabase )
{
int patternCount = customFilter . GetPatternCount ( ) ;
for ( int i = 0 ; i < patternCount ; i + + )
{
string tableNameBuy = PatternTableName ( filterID , PatternName ( i ) , " Buy " ) ;
string tableNameSell = PatternTableName ( filterID , PatternName ( i ) , " Sell " ) ;
dbm . CreateTable ( tableNameBuy , tableschema ) ; // Create table for Buy direction
dbm . CreateTable ( tableNameSell , tableschema ) ; // Create table for Sell direction
}
}
}
return true ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Which order type a given entry price will actually produce. |
//| CExpertTrade::Buy()/Sell() route on price vs ask/bid +- the |
//| SYMBOL_TRADE_STOPS_LEVEL: further out than that in the pending |
//| direction becomes a stop/limit order, anything nearer becomes a |
//| market fill. |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE CExpertSignalCustom : : ResolveOrderType ( bool isLong , double price )
{
2026-08-24 01:39:19 -04:00
return TCResolveOrderType ( m_symbol . Name ( ) , isLong , price , m_symbol . Ask ( ) , m_symbol . Bid ( ) ) ;
2026-08-09 14:51:59 -04:00
}
//+------------------------------------------------------------------+
//| Wrapper functions for buying and selling parameters |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : OpenParams ( bool isLong , double & price , double & sl , double & tp , datetime & expiration )
{
int idx = StartIndex ( ) ;
double atr = m_ATR . Main ( idx ) ;
if ( ! MathIsValidNumber ( atr ) | | atr < = 0.0 )
return false ; // ATR must be positive
if ( ! m_symbol . Name ( _Symbol ) )
return false ; // Symbol information must be accessible
//--- Article 2555 #14: every symbol-property read below (stops level, point, digits) silently
//--- returns 0 for a symbol that is not selected/quoted, which would turn each of the checks
//--- further down into an unconditional pass. Verify the symbol is real and quoted first.
string tc_reason ;
if ( ! TCSymbolIsTradeable ( m_symbol . Name ( ) , tc_reason ) )
{
TraceSignalRejection ( " openparams-symbol: " + m_symbol . Name ( ) ,
__FUNCTION__ + " : rejected - " + tc_reason ) ;
return false ;
}
int lookback_period = m_periods ;
//--- Article 2555 #8: iLowest/iHighest below scan `lookback_period` bars starting at `idx`, and
//--- the ATR read above needs its own warm-up. Rather than discovering the shortfall as a -1
//--- index (handled below) or as a silently truncated scan, check the series depth up front and
//--- let the terminal build the missing history - the next tick finds it ready.
if ( ! TCHasEnoughHistory ( m_symbol . Name ( ) , m_period , lookback_period + idx + m_periods , tc_reason ) )
{
TraceSignalRejection ( " openparams-history: " + m_symbol . Name ( ) ,
__FUNCTION__ + " : rejected - " + tc_reason ) ;
return false ;
}
double base_price = ( m_base_price = = 0.0 ) ? ( isLong ? m_symbol . Ask ( ) : m_symbol . Bid ( ) ) : m_base_price ;
if ( ! MathIsValidNumber ( base_price ) | | base_price < = 0.0 )
return false ; // Price feed must be valid
// Keep swing sourcing strictly bound to this signal's symbol/timeframe. Mixing chart globals
// here can yield index/value mismatches in tester runs and diverge from classic behavior.
int lowest_index = iLowest ( m_symbol . Name ( ) , m_period , MODE_LOW , lookback_period , idx ) ;
int highest_index = iHighest ( m_symbol . Name ( ) , m_period , MODE_HIGH , lookback_period , idx ) ;
2026-08-22 00:25:52 -04:00
//--- Whether the swing prices are actually USED by this configuration. Since 2026-07-31 only
//--- ENTRY_PREV_SWING consumes them - SL and TP are both entry-anchored ATR multiples now. Kept
//--- as guards rather than deleted because a bad swing must still never reach an entry price.
2026-08-09 14:51:59 -04:00
bool needSwings = ( ( int ) m_entry_multiplier = = ENTRY_PREV_SWING_MODE ) ;
if ( needSwings & & ( lowest_index < 0 | | highest_index < 0 ) )
{
// iLowest/iHighest return -1 when the requested history isn't synced yet (thin symbol history,
// timeframe just changed, broker feed gap). Indexing Low()/High() with -1 would otherwise feed
// a bogus swing price into SL/TP below - reject the setup instead.
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-index: " + m_symbol . Name ( ) ,
__FUNCTION__ + " : rejected - iLowest/iHighest returned an invalid index (lowest= " + IntegerToString ( lowest_index ) +
" , highest= " + IntegerToString ( highest_index ) + " ) for " + m_symbol . Name ( ) + " , insufficient history synced. " ) ;
return false ;
}
//--- Index can legitimately be -1 here when !needSwings (the guard above no longer rejects for
//--- it), and iLow/iHigh with a negative index is undefined - so never call it in that case.
double lowest_low = ( lowest_index > = 0 ) ? iLow ( m_symbol . Name ( ) , m_period , lowest_index ) : 0.0 ;
double highest_high = ( highest_index > = 0 ) ? iHigh ( m_symbol . Name ( ) , m_period , highest_index ) : 0.0 ;
if ( needSwings & & ( lowest_low > = DBL_MAX * 0.5 | | highest_high > = DBL_MAX * 0.5 ) )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-sentinel: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - swing prices are sentinel-like (lowest_low=%g, highest_high=%g, symbol=%s, period=%d, low_idx=%d, high_idx=%d). " ,
__FUNCTION__ , lowest_low , highest_high , m_symbol . Name ( ) , m_period , lowest_index , highest_index ) ) ;
return false ;
}
if ( needSwings & & ( ! MathIsValidNumber ( lowest_low ) | | ! MathIsValidNumber ( highest_high ) ) )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-nonfinite: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - swing prices are not finite (lowest_low=%g, highest_high=%g, symbol=%s, period=%d). " ,
__FUNCTION__ , lowest_low , highest_high , m_symbol . Name ( ) , m_period ) ) ;
return false ;
}
if ( needSwings & & ( lowest_low < = 0.0 | | highest_high < = 0.0 ) )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-nonpositive: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - swing prices are non-positive (lowest_low=%g, highest_high=%g, symbol=%s, period=%d). " ,
__FUNCTION__ , lowest_low , highest_high , m_symbol . Name ( ) , m_period ) ) ;
return false ;
}
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- Snapshot the model's conviction for the trade journal. NOTHING BELOW THIS LINE READS IT -
//--- the confidence-scaled entry/SL/TP modes were removed 2026-08-25 (see ConfidenceBridge.mqh).
//--- It is refreshed here, at the last moment before Money is consulted, purely so the journal row
//--- records what the model actually believed at the instant the trade was priced.
PublishConfidenceTelemetry ( ) ;
2026-08-22 00:25:52 -04:00
//--- --- Entry price. Offsets are measured from the CURRENT price (base_price = bid/ask), except
//--- ENTRY_PREV_SWING which anchors to the recent swing.
2026-08-09 14:51:59 -04:00
int entryMode = ( int ) m_entry_multiplier ;
if ( entryMode = = ENTRY_PREV_SWING_MODE )
price = m_symbol . NormalizePrice ( isLong ? lowest_low : highest_high ) ;
else
// Fixed ATR presets: buy => base + mult*ATR (limit below / stop above for -/+ mult);
// sell => base - mult*ATR (limit above / stop below). MARKET (0) leaves price at bid/ask.
price = m_symbol . NormalizePrice ( isLong ? ( base_price + entryMode * atr ) : ( base_price - entryMode * atr ) ) ;
2026-08-22 00:25:52 -04:00
//--- --- Stop loss: always ENTRY-anchored, a straight ATR multiple below (long) / above (short)
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- the entry price. Every SL_ATR_* preset is that multiple, used verbatim - the mode is a number,
//--- not a strategy, which is the whole point of handing it to the GA.
double slMultiplier = ( double ) m_sl_mode ;
2026-08-09 14:51:59 -04:00
sl = isLong ? m_symbol . NormalizePrice ( price - slMultiplier * atr )
: m_symbol . NormalizePrice ( price + slMultiplier * atr ) ;
2026-08-22 00:25:52 -04:00
//--- Enforce a hard minimum SL distance from entry (broker stop-level / sanity floor).
2026-08-09 14:51:59 -04:00
if ( fabs ( price - sl ) < ( MIN_SL_ATR_MULTIPLIER * atr ) )
sl = isLong ? ( price - MIN_SL_ATR_MULTIPLIER * atr ) : ( price + MIN_SL_ATR_MULTIPLIER * atr ) ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- --- Take profit: TP_ATR_* are an ATR multiple FROM THE ENTRY PRICE, used verbatim for the same
//--- reason as the stop above.
double tpMultiplier = ( double ) m_tp_mode ;
tp = isLong ? m_symbol . NormalizePrice ( price + tpMultiplier * atr )
: m_symbol . NormalizePrice ( price - tpMultiplier * atr ) ;
2026-08-09 14:51:59 -04:00
// Guard rail: when both AI and classic share this path, any non-finite or negative level here is an
// upstream data/state issue, not a mode-specific feature. Reject early with full context.
if ( ! MathIsValidNumber ( price ) | | price < 0.0 | |
! MathIsValidNumber ( sl ) | | sl < 0.0 | |
! MathIsValidNumber ( tp ) | | tp < 0.0 )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-invalid-levels: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - invalid computed levels (isLong=%s, entryMode=%d, slMode=%d, tpMode=%d, atr=%g, base=%g, low=%g, high=%g, price=%g, sl=%g, tp=%g). " ,
__FUNCTION__ , isLong ? " true " : " false " , entryMode , m_sl_mode , m_tp_mode ,
atr , base_price , lowest_low , highest_high , price , sl , tp ) ) ;
return false ;
}
2026-08-22 00:25:52 -04:00
//--- --- Article 2555 #6: SL and TP must clear SYMBOL_TRADE_STOPS_LEVEL, measured against the price
//--- of the OPPOSITE operation for a market order (a long closes at Bid, a short at Ask) or against
//--- the activation price for a pending one.
2026-08-09 14:51:59 -04:00
ENUM_ORDER_TYPE order_type = ResolveOrderType ( isLong , price ) ;
string stops_note ;
if ( ! TCAdjustStops ( m_symbol . Name ( ) , order_type , price , sl , tp , stops_note ) )
{
TraceSignalRejection ( " openparams-stops: " + m_symbol . Name ( ) , __FUNCTION__ + " : rejected - " + stops_note ) ;
return false ;
}
if ( stops_note ! = " " )
TraceSignalRejection ( " openparams-stops-adj: " + m_symbol . Name ( ) , __FUNCTION__ + " : " + stops_note ) ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
// (TCAdjustStops() can widen the stop, which changes this trade's real risk. Nothing downstream
// reads that risk any more - the reward:risk recompute that stood here went with its last
// consumer - but the widening itself still matters and is re-verified immediately below.)
2026-08-09 14:51:59 -04:00
// Re-verify rather than trust the correction: TCAdjustStops() widens levels, and a caller that
// hands it a nonsensical pair (SL on the wrong side of the entry) can still come back illegal.
if ( ! TCCheckStops ( m_symbol . Name ( ) , order_type , price , sl , tp , stops_note ) )
{
TraceSignalRejection ( " openparams-stops-final: " + m_symbol . Name ( ) , __FUNCTION__ + " : rejected - " + stops_note ) ;
return false ;
}
// A pending order's own activation price is subject to the same minimum distance. If `price`
// drifted inside it between the entry calculation above and now, CExpertTrade would quietly
// downgrade the order to a market fill at a price the setup never asked for - reject instead.
if ( order_type ! = ORDER_TYPE_BUY & & order_type ! = ORDER_TYPE_SELL & &
! TCCheckPendingPrice ( m_symbol . Name ( ) , order_type , price , stops_note ) )
{
TraceSignalRejection ( " openparams-pending: " + m_symbol . Name ( ) , __FUNCTION__ + " : rejected - " + stops_note ) ;
return false ;
}
// Article 2555 #4: a pending order also has to fit inside ACCOUNT_LIMIT_ORDERS. Checked here,
// before the setup is handed to Money for sizing, so a full order book costs nothing downstream.
if ( order_type ! = ORDER_TYPE_BUY & & order_type ! = ORDER_TYPE_SELL & &
! TCIsNewOrderAllowed ( stops_note ) )
{
TraceSignalRejection ( " openparams-orderlimit " , __FUNCTION__ + " : rejected - " + stops_note ) ;
return false ;
}
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- REWARD:RISK IS NEITHER ENFORCED NOR CONSUMED ANY MORE. The minimum-ratio rejection went with the
//--- Min_Risk_Reward_Ratio input (2026-08-09); the g_TradeRewardRiskRatio bridge that survived it fed
//--- exactly one reader, CMoneyIntelligent's Kelly sizing, and went with that class 2026-08-25. With
//--- SL and TP both literal ATR multiples the ratio is now a fixed property of the two inputs
//--- (TP_Mode / SL_Mode) rather than a per-trade discovery, so there is nothing left to publish: the
//--- GA already sees it directly in the pair of values it is sweeping.
2026-08-09 14:51:59 -04:00
// Adjust expiration time
expiration + = m_expiration * PeriodSeconds ( m_period ) ;
return true ;
}
//+------------------------------------------------------------------+
//| Detecting the levels for buying |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : OpenLongParams ( double & price , double & sl , double & tp , datetime & expiration )
{
return OpenParams ( true , price , sl , tp , expiration ) ;
}
//+------------------------------------------------------------------+
//| Detecting the levels for selling |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : OpenShortParams ( double & price , double & sl , double & tp , datetime & expiration )
{
return OpenParams ( false , price , sl , tp , expiration ) ;
}
//+------------------------------------------------------------------+
//| Common function for closing positions |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckClosePosition ( bool isLong , double & price )
{
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>
2026-08-15 16:18:11 -04:00
//--- Hold-to-barrier: no vote-driven exit of any kind - see m_holdToBarrier's declaration comment.
//--- The base price is still zeroed, exactly as the normal path below does on every call.
if ( m_holdToBarrier )
{
m_base_price = 0.0 ;
return false ;
}
2026-08-09 14:51:59 -04:00
bool result = false ;
//--- check of exceeding the threshold value, adjusted for long/short
double directionMultiplier = isLong ? -1 : 1 ;
revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts a863796 on the operator's call - "unnecessary complexity". It was
right about the mechanism and wrong about the priority: it re-cut the classes
for a case the measured verdict never reaches (SP500 H4 reads "both sides" at
the derived geometry), while the drift that IS happening affects every chart
and every era. Recoverable from a863796 if a one-sided book ever becomes real.
Two pieces of it survive, both independent of the exit idea:
The drift verdict keeps reading m_winLongCache/m_winShortCache rather than the
collapsed label pair. That line reports always-long vs always-short win rates,
which is what the win caches hold - each side scored on its own barriers,
published before the collapse. The label pair carries only the side touched
first, so it undercounted long wins by the both-won-goes-to-short share. There
are zero both-won bars at any geometry with target >= stop, so this changes no
number today; it changes the wrong number to the right one.
And the .cfg gains nothing and loses nothing: the two appended ints go away
again, and they were the last fields, so a .cfg written by yesterday's build
still reads correctly - the loader simply stops before them.
WHAT THE REVERT MAKES ROOM FOR. The operator's actual requirement is that the
model reproduce the label distribution the scan measured, and nothing in the
pipeline ties it to that. The loss trains on a rebalanced sample and the
abstain rate is owned by a margin threshold fitted on EDGE, so the call rate
and the label prior can drift arbitrarily far apart - and did, invisibly:
at era 1350 the models call Buy on 20-28% and Sell on 22-32% of bars against
a scan-measured 2.1% and 4.8%. Roughly a 10x over-call, and not one line in
the journal said so.
The era line now carries it:
CALIBRATION calls vs true rate Buy 28% vs 2% (14.0x) Sell 32% vs 5% (6.4x)
Neutral 40% vs 93% (0.4x)
Reported as a ratio because that is the readable number - 1.0x is calibrated.
This is deliberately a measurement and not yet a correction: matching the
label rate would put coverage near 7%, below the ensemble gate's own 12.4%
coverage floor, so calibration and the gate are in direct conflict and which
one yields is the operator's call, not mine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:14:26 -04:00
// Allowing position closing without checking the prohibition signal.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
if ( directionMultiplier * m_direction > = m_threshold_close )
2026-08-09 14:51:59 -04:00
result = true ;
if ( result )
{
//--- try to get the level of closing, differentiating based on isLong
if ( ! ( isLong ? CloseLongParams ( price ) : CloseShortParams ( price ) ) )
result = false ;
}
//--- zeroize the base price
m_base_price = 0.0 ;
//--- return the result
return result ;
}
//+------------------------------------------------------------------+
//| Generating a signal for closing of a long position |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckCloseLong ( double & price )
{
return CheckClosePosition ( true , price ) ;
}
//+------------------------------------------------------------------+
//| Generating a signal for closing a short position |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckCloseShort ( double & price )
{
return CheckClosePosition ( false , price ) ;
}
//+------------------------------------------------------------------+
//| Common function for opening positions |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckOpenPosition ( bool isLong , double & price , double & sl , double & tp , datetime & expiration )
{
bool result = false ;
//--- the "prohibition" signal
if ( m_prohibition_signal = = true )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-prohibition " ,
StringFormat ( " %s: open %s rejected - a child filter vetoed the tick (prohibition signal). " ,
__FUNCTION__ , isLong ? " long " : " short " ) ) ;
return false ;
}
2026-08-22 00:25:52 -04:00
//--- MARKET-HOURS GATE (2026-08-19). Entries only - exits, SL/TP and the scheduled close-all stay
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>
2026-08-19 11:34:27 -04:00
//--- unguarded on purpose: closing risk must never be blocked by a session boundary.
if ( ! WarriorMarketOpenNow ( m_symbol . Name ( ) , TimeCurrent ( ) ) )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-market-closed " ,
StringFormat ( " %s: open %s rejected - outside the symbol's trading sessions. " ,
__FUNCTION__ , isLong ? " long " : " short " ) ) ;
return false ;
}
2026-08-09 14:51:59 -04:00
//--- check of exceeding the threshold value, adjusted for long/short
double directionMultiplier = isLong ? 1 : -1 ;
if ( directionMultiplier * m_direction > = m_threshold_open )
{
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
//--- VOTE-LEVEL COOLDOWN. Gated HERE, where the vote becomes a trade and the live arrow is
//--- drawn, so suppression means no order AND no arrow - the same "no arrow, no vote, no
//--- position" contract the member-level rule already honours.
if ( ! VoteCooldownAccept ( iTime ( m_symbol . Name ( ) , m_period , 0 ) ) )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-cooldown " ,
StringFormat ( " %s: open %s rejected - inside the %d-bar signal "
" cooldown after the last kept vote. " ,
__FUNCTION__ , isLong ? " long " : " short " ,
WarriorSignalCooldownBars ( ) ) ) ;
return false ;
}
2026-08-09 14:51:59 -04:00
//--- there's a signal
result = true ;
//--- try to get the levels of opening, differentiating based on isLong
if ( ! ( isLong ? OpenLongParams ( price , sl , tp , expiration ) : OpenShortParams ( price , sl , tp , expiration ) ) )
{
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>
2026-08-18 15:41:28 -04:00
//--- FILTERED VIEW, and the reason this arrow is drawn HERE and not where the threshold is
2026-08-22 00:25:52 -04:00
//--- cleared: passing the vote is not the same as trading. A setup can clear
//--- Signal_ThresholdOpen and still never reach the broker - invalid SL/TP, stops-level, ATR
//--- warm-up, unsynced swing history - and every one of those failures lands in this branch.
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>
2026-08-18 15:41:28 -04:00
EraseVoteArrow ( StartIndex ( ) ) ;
2026-08-09 14:51:59 -04:00
// The vote reached the threshold but entry-shaping failed (invalid SL/TP, broker constraints,
// missing history). Roll back one-shot child vote state so the same directional signal can
// be re-offered on the next bar instead of being permanently consumed by this failed attempt.
int total = m_filters . Total ( ) ;
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter ! = NULL )
filter . RevokeVote ( ) ;
}
RevokeVote ( ) ;
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-params-failed " ,
StringFormat ( " %s: open %s rejected after direction passed threshold - order parameters failed validation (vote state restored for retry). " ,
__FUNCTION__ , isLong ? " long " : " short " ) ) ;
result = false ;
}
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>
2026-08-18 15:41:28 -04:00
//--- SURVIVED EVERYTHING: the vote cleared the threshold, no filter vetoed the tick, and the
//--- order parameters validated. THIS is the bar the EA would have placed an order on, so this
//--- is the only place the filtered view may mark. One arrow == one entry the bot would take.
else
if ( ! DrawUnfilteredSignals )
DrawVoteArrow ( StartIndex ( ) , isLong , directionMultiplier * m_direction , sl , tp ) ;
2026-08-09 14:51:59 -04:00
}
else if ( ShouldTraceTradeRejections ( ) )
{
TraceSignalRejection ( " open-threshold " ,
StringFormat ( " %s: open %s rejected - direction %.2f did not reach threshold %.2f. " ,
__FUNCTION__ , isLong ? " long " : " short " , directionMultiplier * m_direction , m_threshold_open ) ) ;
}
//--- zeroize the base price
m_base_price = 0.0 ;
//--- return the result
return result ;
}
//+------------------------------------------------------------------+
//| Generating a buy signal |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckOpenLong ( double & price , double & sl , double & tp , datetime & expiration )
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
// Check if the trading strategy allows opening long positions
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>
2026-08-19 11:10:05 -04:00
if ( WarriorDirectionAllows ( true ) )
2026-08-09 14:51:59 -04:00
{
return CheckOpenPosition ( true , price , sl , tp , expiration ) ;
}
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>
2026-08-19 11:10:05 -04:00
// The effective policy blocks longs
2026-08-09 14:51:59 -04:00
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-long-direction-block " ,
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
StringFormat ( " %s: open long rejected - strategy direction blocks long entries. " , __FUNCTION__ ) ) ;
2026-08-09 14:51:59 -04:00
return false ;
}
//+------------------------------------------------------------------+
//| Generating a sell signal |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckOpenShort ( double & price , double & sl , double & tp , datetime & expiration )
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
// Check if the trading strategy allows opening short positions
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>
2026-08-19 11:10:05 -04:00
if ( WarriorDirectionAllows ( false ) )
2026-08-09 14:51:59 -04:00
{
return CheckOpenPosition ( false , price , sl , tp , expiration ) ;
}
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>
2026-08-19 11:10:05 -04:00
// The effective policy blocks shorts
2026-08-09 14:51:59 -04:00
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-short-direction-block " ,
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
StringFormat ( " %s: open short rejected - strategy direction blocks short entries. " , __FUNCTION__ ) ) ;
2026-08-09 14:51:59 -04:00
return false ;
}
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//| Return the long ladder's matched pattern (consuming read) |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string CExpertSignalCustom : : GetActivePatternLong ( void )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string ret = m_active_pattern_long ;
m_active_pattern_long = " NULL " ;
2026-08-09 14:51:59 -04:00
return ret ;
}
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//| Return the short ladder's matched pattern (consuming read) |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string CExpertSignalCustom : : GetActivePatternShort ( void )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string ret = m_active_pattern_short ;
m_active_pattern_short = " NULL " ;
2026-08-09 14:51:59 -04:00
return ret ;
}
//+------------------------------------------------------------------+
2026-08-23 10:16:30 -04:00
//| SIDE EFFECT 1 of Direction(): journal this filter's matched |
//| patterns to the signal DB. |
//| |
//| Per side, keyed on the ladder having SET a label rather than on |
//| its returned weight - a pattern ranked down to weight 0 by |
//| UpdateSignalsWeights() still FIRED, and gating the row on weight |
//| would freeze a 0%-win-rate pattern out of the very table that |
//| could ever raise it back. The net vote is stored as data |
//| (netVote column), not used as a drop filter. |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : JournalFilterPatterns ( CExpertSignalCustom * filter ,
const MqlDateTime & brokerTime )
{
if ( filter = = NULL | | ! m_useDatabase )
return ;
string filterID = filter . GetFilterID ( ) ;
if ( filterID = = " NULL " )
return ;
double filterNetVote = filter . LastNetVote ( ) ;
string patternLong = filter . GetActivePatternLong ( ) ;
string patternShort = filter . GetActivePatternShort ( ) ;
if ( patternLong ! = " NULL " )
BufferNewTickSignal ( filterID , patternLong , " Buy " , brokerTime , m_symbol . Ask ( ) , filterNetVote ) ;
if ( patternShort ! = " NULL " )
BufferNewTickSignal ( filterID , patternShort , " Sell " , brokerTime , m_symbol . Bid ( ) , filterNetVote ) ;
}
//+------------------------------------------------------------------+
//| SIDE EFFECT 2 of Direction(): the RAW per-model view. |
//| |
//| Classic filters only - an AI member's raw arrows come from its |
//| own cache, not from a live tick. Erasing when neither ladder |
//| matched is deliberate: a stale arrow on a bar that no longer |
//| matches is a lie about what the model sees now. |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : DrawFilterRawView ( CExpertSignalCustom * filter )
{
if ( filter = = NULL | | ! DrawUnfilteredSignals | | filter . IsAIFilter ( ) )
return ;
int rawIdx = filter . StartIndex ( ) ;
string freshLong = filter . PeekActivePatternLong ( ) ;
string freshShort = filter . PeekActivePatternShort ( ) ;
if ( freshLong ! = " NULL " )
filter . DrawRawFilterArrow ( rawIdx , freshLong , true , filter . LastLongWeight ( ) ) ;
else
if ( freshShort ! = " NULL " )
filter . DrawRawFilterArrow ( rawIdx , freshShort , false , filter . LastShortWeight ( ) ) ;
else
filter . EraseRawFilterArrow ( rawIdx ) ;
}
//+------------------------------------------------------------------+
2026-08-09 14:51:59 -04:00
//| Detecting the "weighted" direction |
//+------------------------------------------------------------------+
double CExpertSignalCustom : : Direction ( void )
{
2026-08-22 00:25:52 -04:00
//--- BROKER TIME (2026-08-19, dbVersion 4.0): this one clock stamps every journaled DB row (the
//--- SignalInfo build below) and keys the once-per-second vote window. One clock, the broker's,
//--- everywhere.
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>
2026-08-19 11:37:44 -04:00
MqlDateTime brokerTime ;
datetime nowBroker = TimeCurrent ( brokerTime ) ; // full timestamp AND broken-down form - both are used below
2026-08-09 14:51:59 -04:00
//--- Open a fresh intra-second averaging window whenever the second changes. This block may ONLY
2026-08-22 00:25:52 -04:00
//--- reset the window - it must never be the thing that publishes m_directionLastResult.
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>
2026-08-19 11:37:44 -04:00
if ( nowBroker ! = m_directionCurrentSecond )
2026-08-09 14:51:59 -04:00
{
m_directionAggregatedResult = 0.0 ;
m_directionCount = 0 ;
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>
2026-08-19 11:37:44 -04:00
m_directionCurrentSecond = nowBroker ; // Update the current second
2026-08-09 14:51:59 -04:00
}
m_prohibition_signal = false ;
BeginVote ( ) ; // snapshot any one-shot vote state, so a discarded vote can be rolled back - see BeginVote()
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- Evaluate the two ladders separately and snapshot each one's matched pattern into its own side
//--- slot, keyed on the ladder having SET a label rather than on its returned weight - a pattern
//--- ranked down to weight 0 by UpdateSignalsWeights() still fired, and gating the snapshot on
//--- weight would freeze a 0%-win-rate pattern out of the very table that could ever raise it back.
//--- The scratch is cleared before each call so a stale label from a previous bar (or the other
//--- ladder) can never be attributed to a ladder that matched nothing this bar.
m_active_pattern = " NULL " ;
int longResult = LongCondition ( ) ;
m_active_pattern_long = m_active_pattern ;
m_active_pattern = " NULL " ;
int shortResult = ShortCondition ( ) ;
m_active_pattern_short = m_active_pattern ;
m_lastNetVote = longResult - shortResult ;
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>
2026-08-18 15:41:28 -04:00
m_lastLongWeight = longResult ;
m_lastShortWeight = shortResult ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
double result = m_weight * ( longResult - shortResult ) ;
2026-08-09 14:51:59 -04:00
//--- Non-consuming quorum peek - see m_lastFiredDirection's declaration comment. Snapshotted from
//--- this filter's OWN vote, before the loop below adds any children's contributions in.
m_lastFiredDirection = ( result > 0.0 ) ? 1 : ( ( result < 0.0 ) ? -1 : 0 ) ;
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
//--- Seeded with this signal's OWN vote, on the same condition it always was: a filter with
//--- children of its own is a member of its own consensus. Same accumulator the replay uses -
//--- see SVoteAccumulator for why there is exactly one of these.
SVoteAccumulator vote ;
vote . Add ( result , ( result = = 0.0 ) ? 0.0 : m_weight ) ;
2026-08-09 14:51:59 -04:00
int total = m_filters . Total ( ) ;
PrintVerbose ( " Starting direction calculation with total filters: " + IntegerToString ( total ) ) ;
2026-08-22 00:25:52 -04:00
//--- Pass 1: refresh every filter's own Direction() - required regardless of quorum, since this
//--- is what drives each filter's own training/DB-buffering/m_lastFiredDirection side effects -
//--- caching the returned magnitude for pass 2 below instead of summing it immediately.
2026-08-09 14:51:59 -04:00
double directions [ ] ;
ArrayResize ( directions , total ) ;
bool aborted = false ;
for ( int i = 0 ; i < total ; i + + )
{
long mask = ( ( long ) 1 ) < < i ;
if ( ( m_ignore & mask ) ! = 0 )
{
directions [ i ] = EMPTY_VALUE ;
continue ;
}
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter = = NULL )
{
Print ( " Error: Filter at index " + IntegerToString ( i ) + " is NULL " ) ;
directions [ i ] = EMPTY_VALUE ;
continue ;
}
2026-08-23 10:16:30 -04:00
JournalFilterPatterns ( filter , brokerTime ) ;
2026-08-09 14:51:59 -04:00
double direction = filter . Direction ( ) ;
2026-08-23 10:16:30 -04:00
//--- AFTER the Direction() call, not beside the journaling: the raw view draws what this
//--- evaluation just matched, and before the call that is still last bar's match.
DrawFilterRawView ( filter ) ;
2026-08-09 14:51:59 -04:00
if ( direction = = EMPTY_VALUE )
{
m_prohibition_signal = true ;
directions [ i ] = EMPTY_VALUE ;
continue ;
}
// Validate the result to be within the range of -100 to 100
if ( direction < -100 | | direction > 100 )
{
PrintVerbose ( " A filter's direction is invalid. Skipping tick. " ) ;
result = 0 ;
aborted = true ;
break ;
}
directions [ i ] = direction ;
}
//--- The tick was discarded, so NO filter's vote was used - roll every one of them back, for the same
//--- reason a quorum-suppressed vote is rolled back in pass 2 below (see BeginVote()/RevokeVote()).
if ( aborted )
{
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter ! = NULL )
filter . RevokeVote ( ) ;
}
}
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>
2026-08-19 06:58:03 -04:00
//--- Pass 2: sum each filter's cached contribution, and accumulate the CONSENSUS denominator.
2026-08-09 14:51:59 -04:00
if ( ! aborted )
{
for ( int i = 0 ; i < total ; i + + )
{
double direction = directions [ i ] ;
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>
2026-08-19 06:58:03 -04:00
if ( direction = = EMPTY_VALUE )
2026-08-09 14:51:59 -04:00
continue ;
CExpertSignalCustom * filter = m_filters . At ( i ) ;
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>
2026-08-19 06:58:03 -04:00
//--- The say this filter has, granted by CAPABILITY rather than by participation - see
//--- VoteCapableWeight(). Accumulated before the abstention skip on purpose: an abstainer
//--- dilutes, that is the whole point of consensus.
2026-08-09 14:51:59 -04:00
long mask = ( ( long ) 1 ) < < i ;
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>
2026-08-18 15:52:08 -04:00
double signedDir = ( ( m_invert & mask ) ! = 0 ) ? - direction : direction ;
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
vote . Add ( signedDir , filter . VoteCapableWeight ( ) ) ;
2026-08-09 14:51:59 -04:00
}
}
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
//--- An aborted tick published nothing, so it has no voters and no net - see the rollback above.
int number = aborted ? 0 : vote . voters ;
if ( ! aborted )
result = vote . Net ( ) ;
//--- Normalized by SVoteAccumulator above: the divisor is the CAPABLE weight, so the result reads
//--- as "win-rate estimate x fraction of the ensemble's trust that agrees, net".
2026-08-09 14:51:59 -04:00
//--- Fold this call's result into the current second's window and publish the window average - see
//--- the window-reset block at the top of this function for why this must happen here.
m_directionAggregatedResult + = result ;
m_directionCount + + ;
m_directionLastResult = m_directionAggregatedResult / m_directionCount ;
// Validate the aggregated result to be within the range of -100 to 100
if ( m_directionLastResult < -100 | | m_directionLastResult > 100 )
{
m_directionLastResult = 0.0 ; // Set result to 0 if it's outside the range
Print ( " Directional result is out of range. Setting to 0. " ) ;
}
2026-08-22 00:25:52 -04:00
//--- READOUT, aggregate only. Placed AFTER the range check so the label shows what the threshold
//--- is actually tested against, not a pre-clamp value.
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>
2026-08-18 18:22:45 -04:00
if ( total > 0 )
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 155f56e is live and working:
"Filtered view: swept 4999 bar(s), 767 had a voter, drew 0 arrow(s).
Strongest vote 36.0% against a 40.0% threshold."
4,999 bars against the previous 0. The remaining emptiness is the models, not
the plumbing - see the reply for why lowering the threshold further is the
wrong response to it.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:29:27 -04:00
{
2026-08-22 00:25:52 -04:00
//--- NOBODY VOTED - and by far the most common reason is that no model is DEPLOYED yet, not
//--- that they all abstained.
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
m_lastLiveVoters = number ;
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>
2026-08-18 21:18:55 -04:00
//--- neutrals = -1: the live pass does not track how many filters answered Neutral (they are
//--- skipped in pass 2 without a count), so the label shows the plain voter count here.
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
if ( number > 0 )
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>
2026-08-18 21:18:55 -04:00
UpdateVoteReadout ( m_directionLastResult , number , -1 , false ) ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
else
RefreshVoteReadout ( ) ;
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 155f56e is live and working:
"Filtered view: swept 4999 bar(s), 767 had a voter, drew 0 arrow(s).
Strongest vote 36.0% against a 40.0% threshold."
4,999 bars against the previous 0. The remaining emptiness is the models, not
the plumbing - see the reply for why lowering the threshold further is the
wrong response to it.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:29:27 -04:00
}
2026-08-09 14:51:59 -04:00
PrintVerbose ( " Final directional result: " + DoubleToString ( m_directionLastResult ) ) ;
return m_directionLastResult ;
}
//+------------------------------------------------------------------+
//| handles the new bar signal buffering |
//+------------------------------------------------------------------+
fix(build): four compile faults - one was a SILENT enum collision that inverted the direction policy
Reported by the user's MetaEditor compile of f64e0f8 (26 errors, 4 warnings). The four
warnings mattered more than the errors.
1. INTELLIGENT WAS TWO ENUMS. MONEY_MANAGEMENT_STRATEGY::INTELLIGENT (=1) is declared
BEFORE TRADING_DIRECTION::INTELLIGENT (=3) in InputEnums.mqh, so MQL5 resolved every
'tradingdirection == INTELLIGENT' to the MM member and converted it to value 1 =
TRADING_DIRECTION::LONG_ONLY. Wrong in both directions at once: selecting Intelligent
(3) matched NOTHING and silently traded both sides, while selecting Long only (1)
matched and handed the decision to the measured drift verdict - which can answer
SHORT_ONLY, so the one setting that must never go short could have. Reported by the
compiler as a WARNING only, never an error. Renamed to DIRECTION_INTELLIGENT; the
VALUE stays 3, so saved .set files are unaffected. Swept every enum in the repo for
sibling collisions (38 enums, detector validated against the pre-fix source, which it
flags): none remain.
2. g_warriorMetaGate sits above the class it points at - added the forward declaration,
the same pattern g_warriorEnsemble already uses in ExpertSignalAIBase.mqh.
3. The broker-time rename (b63e39f) never reached BufferNewTickSignal's PARAMETER or its
two call sites: the local became brokerTime, the parameter stayed gmtTime, and the
body was rewritten to read brokerTime. All five sites now agree.
4. ConfigureAISignal calls IsMetaTarget() from a free function - moved it to the public
section (identity, not an implementation seam); the other meta seams stay protected.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:16:45 -04:00
void CExpertSignalCustom : : BufferNewTickSignal ( string filterID , string pattern , string bias , const MqlDateTime & brokerTime , double entryPrice , double netVote )
2026-08-09 14:51:59 -04:00
{
if ( filterID = = " NULL " | | pattern = = " NULL " | | bias = = " NULL " )
{
Print ( " Error buffering new tick signal: Invalid filter parameters - filterID: ' " + filterID +
" ', pattern: ' " + pattern + " ', bias: ' " + bias + " '. " ) ;
return ;
}
string tableName = PatternTableName ( filterID , pattern , bias ) ;
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>
2026-08-19 11:37:44 -04:00
SignalInfo signal = { brokerTime . year , brokerTime . mon , brokerTime . day , brokerTime . day_of_week , brokerTime . hour , brokerTime . min , tableName , pattern , bias , entryPrice , netVote } ;
2026-08-09 14:51:59 -04:00
BufferSignal ( signal ) ;
PrintVerbose ( " New tick signal buffered: " + tableName + " , Pattern: " + pattern + " , Bias: " + bias + " , Entry Price: " + DoubleToString ( entryPrice ) ) ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : BufferSignal ( SignalInfo & signal )
{
// Check for duplicate signals in the buffer
for ( int i = 0 ; i < ArraySize ( signalBuffer ) ; i + + )
{
if ( signalBuffer [ i ] . tableName = = signal . tableName & &
signalBuffer [ i ] . pattern = = signal . pattern & &
signalBuffer [ i ] . direction = = signal . direction )
{
PrintVerbose ( " Duplicate signal detected, not adding to buffer: " + signal . tableName + " , Pattern: " + signal . pattern + " , Direction: " + signal . direction ) ;
return ; // Skip buffering if a duplicate is found
}
}
// Resize the buffer and add the new signal
ArrayResize ( signalBuffer , ArraySize ( signalBuffer ) + 1 ) ;
signalBuffer [ ArraySize ( signalBuffer ) - 1 ] = signal ;
PrintVerbose ( " Signal buffered for: " + signal . tableName + " , Pattern: " + signal . pattern + " , Direction: " + signal . direction ) ;
}
//+------------------------------------------------------------------+
//| Process the signal and update trades |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : ProcessSignal ( SignalInfo & signal )
{
string currentTableName = signal . tableName ;
string oppositeTableName = currentTableName ; // Start with a copy of the current table name
PrintVerbose ( " Processing signal for table: " + currentTableName ) ;
// Swap the direction in the table name to get the opposite table name
if ( signal . direction = = " Buy " )
{
StringReplace ( oppositeTableName , " Buy " , " Sell " ) ;
PrintVerbose ( " Swapped to opposite table: " + oppositeTableName + " from Buy to Sell " ) ;
}
else
{
StringReplace ( oppositeTableName , " Sell " , " Buy " ) ;
PrintVerbose ( " Swapped to opposite table: " + oppositeTableName + " from Sell to Buy " ) ;
}
2026-08-22 00:25:52 -04:00
//--- Every question below is answered by a targeted SQL lookup returning one row or one number.
//--- Per-signal cost is now flat in table size.
2026-08-12 18:53:04 -04:00
int curCount = 0 , oppCount = 0 ;
if ( ! dbm . FetchRecordCount ( currentTableName , curCount ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( " Failed to count current direction trades in: " + currentTableName ) ;
return ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
if ( ! dbm . FetchRecordCount ( oppositeTableName , oppCount ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( " Failed to count opposite direction trades in: " + oppositeTableName ) ;
return ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
if ( curCount > = m_maxTableRows )
2026-08-09 14:51:59 -04:00
DeleteOldestEntry ( currentTableName ) ;
2026-08-12 18:53:04 -04:00
if ( oppCount > = m_maxTableRows )
2026-08-09 14:51:59 -04:00
DeleteOldestEntry ( oppositeTableName ) ;
2026-08-22 00:25:52 -04:00
//--- Close the opposite direction's open trade, if any. Closing does NOT absorb the signal: the
//--- reversing signal still registers its own trade below (true stop-AND-reverse). State patterns
//--- escaped only by re-firing one bar later. The side that never registered also never got a win
//--- rate, so UpdateSignalsWeights() weighted the pattern from one side only.
2026-08-12 18:53:04 -04:00
string oppositeDirection = ( signal . direction = = " Buy " ) ? " Sell " : " Buy " ;
double oppEntry = 0.0 ;
bool oppOpen = false ;
if ( ! dbm . FetchOpenTradeEntry ( oppositeTableName , signal . pattern , oppositeDirection , oppEntry , oppOpen ) )
return ;
if ( oppOpen )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
double profitLoss = ( oppositeDirection = = " Buy " ) ? ( signal . entryPrice - oppEntry )
: ( oppEntry - signal . entryPrice ) ;
TradeRecord closeRec ;
closeRec . pattern = signal . pattern ;
closeRec . direction = oppositeDirection ;
closeRec . exitPrice = signal . entryPrice ;
closeRec . result = profitLoss > = 0 ? " Profit " : " Loss " ;
UpdateTradeRecordInDatabase ( oppositeTableName , closeRec ) ;
PrintVerbose ( " Closed opposite trade: " + oppositeTableName + " , Profit/Loss: " + DoubleToString ( profitLoss ) ) ;
2026-08-09 14:51:59 -04:00
}
2026-08-22 00:25:52 -04:00
//--- Duplicate / outdated / out-of-order guard: rows are inserted in chronological order, so the
//--- newest row (max ROWID) carries the table's latest timestamp; a signal at or before it is a
//--- duplicate or a replay and must not register.
2026-08-12 18:53:04 -04:00
long newestKey = 0 ;
bool hasRows = false ;
if ( ! dbm . FetchNewestTimeKey ( currentTableName , newestKey , hasRows ) )
return ;
long sigKey = SignalTimeKey ( signal . year , signal . month , signal . day , signal . hour , signal . minutes ) ;
if ( hasRows & & newestKey > = sigKey )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
PrintVerbose ( " Duplicate or outdated signal, not registering. Table: " + currentTableName ) ;
return ;
}
// One open trade per pattern+side at most
double curEntry = 0.0 ;
bool curOpen = false ;
if ( ! dbm . FetchOpenTradeEntry ( currentTableName , signal . pattern , signal . direction , curEntry , curOpen ) )
return ;
if ( curOpen )
{
PrintVerbose ( " Open trade found, not registering new trade. Table: " + currentTableName + " , Pattern: " + signal . pattern ) ;
return ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 11:32:36 -04:00
// Register a new trade if no duplicates, outdated, or open trades were found above
RegisterSignal ( signal . year , signal . month , signal . day , signal . DOW , signal . hour , signal . minutes ,
currentTableName , signal . pattern , signal . direction , signal . entryPrice , 0.0 , " NA " , signal . netVote ) ;
PrintVerbose ( " Registered new trade in table: " + currentTableName + " , Pattern: " + signal . pattern + " , Direction: " + signal . direction ) ;
2026-08-09 14:51:59 -04:00
}
//+------------------------------------------------------------------+
2026-08-12 18:53:04 -04:00
//| yyyymmddhhmm as a number - the ordering key the targeted DB |
//| lookups compare on (matches the SQL expression they compute) |
//+------------------------------------------------------------------+
long SignalTimeKey ( const int year , const int month , const int day , const int hour , const int minutes )
{
return ( ( ( ( long ) year * 100 + month ) * 100 + day ) * 100 + hour ) * 100 + minutes ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| ONE LINE, TOP-RIGHT: the vote that is actually being tested. |
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>
2026-08-18 18:22:45 -04:00
//+------------------------------------------------------------------+
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>
2026-08-18 21:18:55 -04:00
void CExpertSignalCustom : : UpdateVoteReadout ( const double vote , const int voters , const int neutrals ,
const bool prospective )
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>
2026-08-18 18:22:45 -04:00
{
double mag = MathAbs ( vote ) ;
2026-08-25 15:43:22 -04:00
//--- m_votePeak is still tracked (Signal_ThresholdOpen tuning still wants it available
//--- programmatically) but no longer printed here - see the trimmed format below
//--- (user request 2026-08-25: drop peak/need/armed from the live line).
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>
2026-08-18 18:22:45 -04:00
if ( MathIsValidNumber ( mag ) & & mag > m_votePeak )
m_votePeak = mag ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- A PROSPECTIVE vote can never be a trade, however high it reads: it is a recomputation between
//--- bars, not the bar's own decision. Saying "-> TRADE" on a number that cannot place an order
//--- would be the exact overstatement this readout exists to prevent.
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>
2026-08-19 11:10:05 -04:00
bool fires = ( mag > = m_threshold_open ) & & ( voters > 0 ) & & ! prospective & &
( vote = = 0.0 | | WarriorDirectionAllows ( vote > 0.0 ) ) ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- DEPLOYMENT IS A PROPERTY OF THE MODELS, NOT OF WHICH CODE PATH PRODUCED THIS NUMBER. The two
//--- were conflated: `prospective` alone drove the verdict word, so a fully deployed ensemble on a
//--- bar where every member abstained - which is precisely when the prospective path runs - was
//--- told it was still training, directly under four member lines saying "Live".
bool deployed = WarriorChartModelsDeployed ( ) ;
2026-08-22 00:25:52 -04:00
//--- THE HEADLINE WORD IS THE DECISION, NOT THE LEAN (user request 2026-08-19).
2026-08-19 10:24:21 -04:00
bool clears = ( voters > 0 ) & & ( vote ! = 0.0 ) & & ( mag > = m_threshold_open ) ;
string dir = ( voters < = 0 & & neutrals < = 0 ) ? " -- "
: ( clears ? ( vote > 0.0 ? " BUY " : " SELL " ) : " NEUTRAL " ) ;
2026-08-25 15:43:22 -04:00
//--- TWO STATES ONLY (user request 2026-08-25: drop the "armed (bar still open)" middle state).
//--- A prospective vote on a deployed model now simply reads "no trade" like any other bar that
//--- did not clear - fires is already forced false while prospective, so this falls out for free.
string verdict = ! deployed ? " -> training, not tradable yet " : ( fires ? " -> TRADE " : " -> no trade " ) ;
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>
2026-08-18 21:18:55 -04:00
//--- "2 vote/2 flat" rather than a bare count: which members are Neutral is half of what the
//--- label is watched for during training.
string who = ( neutrals > = 0 )
? StringFormat ( " %d vote/%d flat " , voters , neutrals )
: StringFormat ( " %d voter(s) " , voters ) ;
feat(panel): one live vote line, no stale era count, no per-model HUD
Two chart-display fixes reported after watching a converged 4-model
ensemble: the ensemble panel's trailing "(era 69, 4 models,
DEPLOYING)" was frozen at whatever era the ensemble happened to
deploy on, and the separate top-right HUD (one line per model, raw
B/S/N + weight + era + error) was clutter once the vote itself is
what matters.
Root cause of the freeze: g_ensembleVoteLine is written once per era,
at pass-3 completion. A deployed/converged ensemble runs no further
eras (ScheduleTrainingIfNeeded's trainingComplete branch skips
Train() entirely), so that line could never update again - the era
count and "DEPLOYING" marker were permanent set-dressing from the
deploying era, not a live reading.
- EnsembleScoreCombinedVote() drops the era/DEPLOYING tail once
g_ensDeployApproved - nothing left there worth freezing.
- UpdateVoteReadout() (the aggregate "VOTE ..." line, previously its
own top-right chart object) now writes g_liveVoteLine instead of
drawing anything. Both status-label builders - PublishEnsembleStatus
for the ensemble panel, PublishStatus's choke point for the solo
panel - append it as one line, refreshed every tick/timer exactly
as the old HUD was, so the live vote replaces the frozen era tail
in the same visual slot.
- RefreshVoteReadout()'s per-member loop (DisplayHudLine, one
ObjectLabel per model) is deleted outright rather than folded in -
the operator asked for the aggregate only, "without telling me each
individual network".
Follow-on dead-code removal, since DisplayHudLine was the only
caller: the DispProb/DispSignal/MetaGateArmedNow/MetaHasScore/
MetaLastP/MetaLastBe/MetaApproved/MetaVetoed leg of IChartView (and
its AIBaseChartView/AIBaseChartViewImpl/ExpertSignalAIBase forwards)
had no other reader. The underlying data survives untouched -
m_metaTelemetry is still populated live by SignalMETA.mqh,
m_dispSignal still feeds ProspectiveVote - only the chart-view
forwarding that existed solely to reach the deleted HUD is gone.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:27:41 -04:00
//--- Word, not colour, carries the verdict now that this line lives in the plain-text status
//--- label instead of its own chart object: "verdict" above already says TRADE/no trade/training.
2026-08-25 15:43:22 -04:00
g_liveVoteLine = StringFormat ( " Live vote: %s %+5.1f%% %s %s " , dir , vote , who , verdict ) ;
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>
2026-08-18 18:22:45 -04:00
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Repaint the readout from the CURRENT prospective vote. |
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
//+------------------------------------------------------------------+
void CExpertSignalCustom : : RefreshVoteReadout ( void )
{
int total = m_filters . Total ( ) ;
if ( total < = 0 )
return ; // leaf filter: the readout belongs to the aggregate alone
if ( m_lastLiveVoters > 0 )
return ; // a real vote is on display; it owns the label until the next bar
double pNum = 0.0 , pDen = 0.0 ;
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>
2026-08-18 21:18:55 -04:00
int pVoters = 0 , pFlats = 0 ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
for ( int i = 0 ; i < total ; i + + )
{
long mask = ( ( long ) 1 ) < < i ;
if ( ( m_ignore & mask ) ! = 0 )
continue ;
CExpertSignalCustom * f = m_filters . At ( i ) ;
if ( f = = NULL )
continue ;
double pv = 0.0 , pw = 0.0 ;
if ( ! f . ProspectiveVote ( pv , pw ) | | pw < = 0.0 )
continue ;
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>
2026-08-19 06:58:03 -04:00
//--- CONSENSUS: the weight lands in the denominator for every model WITH a decision - a
//--- Neutral dilutes the mean exactly as it does in the live vote and the overlay, so the
//--- label, the arrows and the trade all move together.
pDen + = pw ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
if ( pv = = 0.0 )
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>
2026-08-18 21:18:55 -04:00
{
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>
2026-08-19 06:58:03 -04:00
pFlats + + ; // has a decision, and it is Neutral: dilutes the mean, shows in the count
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>
2026-08-18 21:18:55 -04:00
continue ;
}
pVoters + + ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
pNum + = ( ( m_invert & mask ) ! = 0 ) ? - pv : pv ;
}
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>
2026-08-18 21:18:55 -04:00
if ( pVoters + pFlats < = 0 )
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
return ; // nothing to say yet; leave whatever the label holds
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>
2026-08-18 21:18:55 -04:00
UpdateVoteReadout ( ( pDen > 0.0 ) ? ( pNum / pDen ) : 0.0 , pVoters , pFlats , true ) ;
fix(chart): the vote readout was repainted once per bar, not once per timer tick
"Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new
build was running, so this was not a stale binary.
The readout was only ever written inside Direction(), and with
Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and
therefore Direction() - to NEW-BAR ticks (verified in the terminal's own
Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a
period boundary). On an H4 chart that is one repaint every four hours. The
label was written exactly once at attach - before any model had produced a
decision, so it read 0.0 with 4 models - and then sat frozen while the models
trained underneath it. "Stuck at 0" was the label's refresh RATE, not the
vote's value. The prospective fallback in a042cb4 was correct and running;
it just had no way to reach the screen until the next bar open.
The prospective computation is extracted into RefreshVoteReadout(), called
from OnTimer through CExpertCustom every timer tick. It defers to the trade
path whenever the last real Direction() had live voters (m_lastLiveVoters
latch): a live vote is authoritative for its whole bar, and repainting
prospective numbers over it would overwrite a tradable reading with an
untradable one. Cheap by construction - a handful of filters, plain
arithmetic on already-computed members, no indicator reads - so it belongs on
the 500ms timer without a throttle.
Expect the label to move at timer cadence now, tracking pass-1's walk through
the training window (dPrevSignal holds the last trained bar's output during
an era), dimmed and labelled "training, not tradable yet".
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
}
//+------------------------------------------------------------------+
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//| ARM the historical rebuild of the filtered view. |
//| |
//| Called at init and again whenever an era ends, because an era end |
//| is exactly when the answer changes: the nets' weights moved, and |
//| RankTiersFromOos() has just re-derived every tier's vote weight |
//| from that era's holdout. A reconstruction built from the previous |
//| era's weights is a picture of a model that no longer exists. |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : StartFilteredOverlay ( void )
{
if ( DrawUnfilteredSignals )
return ; // raw view: the per-model layer owns the chart, nothing to reconstruct
int barsAvail = Bars ( m_symbol . Name ( ) , m_period ) ;
if ( barsAvail < = 300 )
return ;
//--- Same bound the "Show signals" rescan uses, for the same reason: full history is not free and
//--- the terminal's own "Max bars in chart" makes anything past it undrawable anyway.
int span = MathMin ( SIGNAL_RESCAN_LOOKBACK_BARS , barsAvail ) ;
2026-08-22 00:25:52 -04:00
//--- BOTH BOUNDS ARE SERIES INDICES - 0 is the newest bar and the index counts BACKWARDS in
//--- time.
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 129a0d4 is what made this findable - a blank chart
that cannot say why is indistinguishable from a broken one, and this was the
first thing it caught.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:17:25 -04:00
m_overlayIndex = MathMin ( span , barsAvail - 150 ) ;
//--- Stop at 2, not 0: bar 0 is still forming and bar 1 is the decision bar the FORWARD path
//--- owns. The handover-time check inside the sweep covers this too, belt and braces.
m_overlayStopIndex = 2 ;
if ( m_overlayIndex < m_overlayStopIndex )
return ; // not enough history past the warm-up tail to reconstruct anything
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- Latch the handover point ONCE. On later rebuilds the cutoff must stay where the EA actually
//--- took over, not creep forward to "now" and start overwriting real decisions with guesses.
if ( m_overlayLiveCutoff = = 0 )
m_overlayLiveCutoff = iTime ( m_symbol . Name ( ) , m_period , 0 ) ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
m_overlaySweptBars = 0 ;
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:58:34 -04:00
m_overlayHadDataBars = 0 ;
2026-08-26 16:53:16 -04:00
m_overlaySnapNoWeightBars = 0 ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
m_overlayVotedBars = 0 ;
m_overlayDrawn = 0 ;
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
m_overlayVotedBuy = 0 ;
m_overlayVotedSell = 0 ;
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>
2026-08-19 06:58:03 -04:00
m_overlayNmsLastBuyIdx = -1 ;
m_overlayNmsLastSellIdx = -1 ;
m_overlayNmsKeptIdx = -1 ;
m_overlayNmsKeptBuy = false ;
m_overlayNmsKeptNet = 0.0 ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
m_overlayBestNet = 0.0 ;
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
m_overlayVoteFired = 0 ;
m_overlayVoteWins = 0 ;
m_overlayScoreReady = false ;
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>
2026-08-18 21:18:55 -04:00
//--- The readout's peak resets HERE, at the same regime boundary that resets the census: tier
//--- weights have just been re-derived, and a peak attained under the previous weights is not
2026-08-22 00:25:52 -04:00
//--- comparable to anything the new weights can produce.
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>
2026-08-18 21:18:55 -04:00
m_votePeak = 0.0 ;
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
//--- The sweep records what it drew so the prune below can re-walk it in FORWARD time order.
m_overlayVoteCount = 0 ;
ArrayResize ( m_overlayVoteTime , 0 ) ;
ArrayResize ( m_overlayVoteBuy , 0 ) ;
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
m_overlayPending = true ;
}
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//+------------------------------------------------------------------+
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>
2026-08-23 09:06:46 -04:00
//| THE VOTE ON A HISTORICAL BAR - one aggregation rule, one divisor. |
//| |
//| This is the replay half of Direction(). It is separate for ONE |
//| reason: Direction() is not a query, it is a transaction. It |
//| journals DB rows, draws raw arrows, folds the result into an |
//| intra-second averaging window, consumes one-shot per-filter vote |
//| state and refreshes the live readout. Every one of those is |
//| wrong on a bar from three weeks ago, which is why the classic |
//| replay below has to bracket its Direction() call in a six-field |
//| save/restore - that bracket IS the evidence, and it goes away |
//| when Direction() is split into a pure vote plus its side effects. |
//| |
//| What must NOT differ between here and live is the arithmetic, and |
//| it did: this loop used ModuleWeight() as the divisor while live |
//| uses VoteCapableWeight(). A meta head (structurally incapable of |
//| voting) and an untrained member both return 0 from the latter and |
//| their full weight from the former, so every reconstructed vote |
//| was shrunk by members that could never agree. The comment on the |
//| old line even said "capable weight" while the code said |
//| ModuleWeight - read the code for the value, the comment for the |
//| why, and when they disagree the code is what shipped. |
//| |
//| capableOut returns the divisor so the caller can tell "nobody had |
//| anything to say about this bar" (0) from "the vote was neutral". |
//+------------------------------------------------------------------+
2026-08-26 16:53:16 -04:00
double CExpertSignalCustom : : HistoricalNetVote ( const int idx , double & capableOut , bool & hadSnapshotOut )
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>
2026-08-23 09:06:46 -04:00
{
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
SVoteAccumulator vote ;
2026-08-26 16:53:16 -04:00
hadSnapshotOut = false ;
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>
2026-08-23 09:06:46 -04:00
int total = m_filters . Total ( ) ;
for ( int i = 0 ; i < total ; i + + )
{
long mask = ( ( long ) 1 ) < < i ;
if ( ( m_ignore & mask ) ! = 0 )
continue ;
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter = = NULL )
continue ;
double contribution = 0.0 ;
bool hasData = false ;
if ( filter . IsAIFilter ( ) )
{
//--- ERA-END SNAPSHOT, not the live cache, and the difference was a chart that flickered
//--- between populated and blank.
hasData = filter . SnapshotVoteAt ( idx , contribution ) ;
}
else
if ( filter . GetPatternCount ( ) < = 0 )
continue ; // veto filter (news/session/risk guard): no vote, no replay
else
{
//--- Replay, with the live journaling state saved across it - see SaveVoteState().
string pl , ps ; double nv ; int lw , sw , fd ;
filter . SaveVoteState ( pl , ps , nv , lw , sw , fd ) ;
filter . EvalShift ( idx ) ;
filter . Direction ( ) ;
filter . EvalShift ( 0 ) ;
double signedWeight = ( double ) ( filter . LastLongWeight ( ) - filter . LastShortWeight ( ) ) ;
filter . RestoreVoteState ( pl , ps , nv , lw , sw , fd ) ;
contribution = filter . ModuleWeight ( ) * signedWeight ;
hasData = true ; // a ladder always answers; "no match" is an abstention
}
//--- NO SNAPSHOT IS NOT AN ABSTENTION. An abstainer looked at the bar and said nothing, and
//--- must dilute the consensus; a member with no snapshot has not looked, and must not be in
//--- the divisor at all - see g_warriorOverlayReadyMask.
if ( ! hasData )
continue ;
2026-08-26 16:53:16 -04:00
//--- SEPARATE FROM THE DIVISOR ON PURPOSE. A member can have a snapshot and still contribute
//--- nothing, because ReconstructionWeight() is zero for a member with no demonstrated edge.
//--- The census used to collapse the two and report "not one bar had a snapshot" for both,
//--- which sent three separate investigations after a publication fault that did not exist.
hadSnapshotOut = true ;
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>
2026-08-23 09:06:46 -04:00
if ( ( m_invert & mask ) ! = 0 )
contribution = - contribution ;
feat(deploy): ship on positive EXPECTANCY, and let the chart draw before convergence
TWO CHANGES, both of which turn a permanent "nothing happens" into a decision.
1. THE DEPLOY GATE ASKS THE WRONG QUESTION. tradeable required the win rate to
clear chance by EDGE_MIN_SIGMAS - "can I PROVE an edge exists" from one OOS
window. On H4 that asks ~66% against a market supplying ~53%, so it is
unreachable by construction and no run has ever deployed through it.
SDeployVerdict now also carries the economics of the geometry actually being
traded - cost-adjusted break-even and reward:risk, both from the new
CostAdjustedGeometry() so a spread convention cannot be applied to one and
missed on the other - and derives
E[R] = (p - p*) * (1 + RR)
which is exactly zero at break-even by construction, so "profitable" and
"beats break-even" can never disagree. Under DeployOnExpectancy (new input,
default ON) tradeable becomes E[R] > 0 and selectionScore ranks eras by
expectancy instead of precision. Coverage and both-sides-live still gate
both: an expectancy over a handful of one-sided calls is not tradeable.
The struct also publishes scoreSE - the SE of selectionScore IN THE SCORE'S
OWN UNITS - because the score changes units with the objective (win-rate
points vs R). Both plateau bands now read it instead of precSE, which was
right for one objective and dimensionally wrong for the other.
Setting DeployOnExpectancy=false restores the previous behaviour exactly.
2. THE FILTERED VIEW COULD NOT DRAW WHILE ANY MODEL WAS TRAINING.
HistoricalNetVote built its divisor from VoteCapableWeight(), which answers
"may this member move real money" and returns 0.0 for an AI member until the
whole run converges. So the reconstruction's divisor was zero on EVERY bar,
every bar was skipped as "nobody looked", and the chart drew nothing at all -
for the entire training run, which before the plateau noise band was forever.
Reported as "no signals drawn since the refactor".
New ReconstructionWeight(): the same weight WITHOUT the converged-run
requirement, overridden on the AI member to ModuleWeight() gated on
SelfRanked() only. The overlay is a picture of what the vote WOULD have
shown, which a mid-training model can answer - the chart HUD already says so
with its "(trn)" marker. Live Direction() still uses VoteCapableWeight(), so
no untrained model gains a say in an order.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:15:06 -04:00
//--- ReconstructionWeight, NOT VoteCapableWeight - see its declaration. This is the ONLY
//--- divisor in the file that is allowed to differ from live, and it differs in exactly one
//--- way: it does not require the training run to have converged.
vote . Add ( contribution , filter . ReconstructionWeight ( ) ) ;
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>
2026-08-23 09:06:46 -04:00
}
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 7881159 had to patch by hand.
- only a non-zero contribution counts as a VOTER, which is what the
readout's "N voter(s)" means.
Behaviour is unchanged on the live path: same seeding condition for this
signal's own vote, same capable weight per filter, same divisor. The
abort path no longer assigns `number` before it exists - it is derived
from the accumulator afterwards instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:15:06 -04:00
capableOut = vote . capable ;
return vote . Net ( ) ;
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>
2026-08-23 09:06:46 -04:00
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| RECONSTRUCT what the filtered view would have shown, one chunk |
//| per call. Returns true while there is more to do. |
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : AdvanceFilteredOverlay ( const int barBudget )
{
if ( ! m_overlayPending )
return false ;
if ( DrawUnfilteredSignals ) // switched to the raw view mid-sweep
{
m_overlayPending = false ;
return false ;
}
int processed = 0 ;
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 129a0d4 is what made this findable - a blank chart
that cannot say why is indistinguishable from a broken one, and this was the
first thing it caught.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:17:25 -04:00
while ( m_overlayIndex > = m_overlayStopIndex & & processed < barBudget )
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
{
2026-08-22 00:25:52 -04:00
//--- STOP CHECK PER BAR, not per slice. The slice bound alone is not a stop check: it bounds
//--- throughput, not latency.
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 07aa017.
THE ONE I ADDED. The filtered view's overlay draws up to
SIGNAL_RESCAN_LOOKBACK_BARS vote arrows. OnDeinit's EARLY VISIBLE-UI SWEEP
runs with skipArrows=true, which skips any prefix equal to SIG_ARROW_PREFIX -
and "WarSig_VOTE_..." starts with "WarSig_", so every one of them was skipped
by the one sweep that is cheap enough to always complete. They then sat in the
object list while the two expensive scans that follow walked it: a per-member
SaveChartSignals O(total) scan, then the by-name rescan. On a chart with years
of history that is thousands of extra objects walked twice, inside a teardown
budget measured from the stop REQUEST rather than from OnDeinit's first line.
skipArrows exists because the per-model arrows' sidecar is rebuilt by SCANNING
them off the chart, so they cannot be deleted before that write. Vote arrows
have no sidecar - they are a reconstruction, rebuilt on the next attach - so
nothing is preserving them and they now get their own prefix slot, deleted by
one native call in the first few milliseconds.
THE FIVE LOOPS. A time budget bounds THROUGHPUT, not latency to an unload, and
OnDeinit cannot begin until whatever is in flight returns. These all scaled
with history and none of them checked:
* Training passes 2, 2.5 and 3 yielded only on TRAIN_TIME_BUDGET_MS. Pass 1
has checked IsStopped() all along; the other three never have, and they
are the ones that grow with the bar count. Free to fix - the resume state
is written either way, so a stopped chunk simply is not re-entered.
* PruneDirectionalClusters: the one UNCHUNKED sweep left, once per era over
every bar, with its own header noting that raising the training budget
cannot help its cost. Now bails outright.
* AdvanceChartSignalRestore / AdvanceChartSignalRescan: chunked, but the
rescan runs a full feedForward per bar over up to 5000 bars and the
restore can hold MAX_RESTORED_ARROWS entries. Checked on the same
64-object stride as the clock read, since the check is not free either.
* AdvanceFilteredOverlay (mine, 07aa017) replays Direction() on every
classic filter per bar and had no check at all. Now per bar.
ChartUI.mqh had ZERO shutdown checks across six loops before this.
Nothing was added to the purge path itself: that is the work that must
complete, and an IsStopped() check inside it would abort unconditionally -
IsStopped() is already true by the time OnDeinit runs.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 17:13:50 -04:00
if ( IsStopped ( ) )
{
m_overlayPending = false ;
return false ;
}
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
int idx = m_overlayIndex - - ;
processed + + ;
datetime bt = iTime ( m_symbol . Name ( ) , m_period , idx ) ;
//--- At or past the handover: the forward path owns these bars. Leave whatever it decided.
if ( bt < = 0 | | ( m_overlayLiveCutoff > 0 & & bt > = m_overlayLiveCutoff ) )
continue ;
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>
2026-08-23 09:06:46 -04:00
double den = 0.0 ;
2026-08-26 16:53:16 -04:00
bool hadSnapshot = false ;
double net = HistoricalNetVote ( idx , den , hadSnapshot ) ;
if ( den < = 0.0 & & hadSnapshot )
m_overlaySnapNoWeightBars + + ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
//--- Census for the completion line below - see it for why a blank chart has to be able to
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
//--- say WHY it is blank. The buy/sell split exists because "the vote leans one way" must be
//--- checkable from the log, not inferred from squinting at arrow colours.
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:58:34 -04:00
if ( den > 0.0 )
m_overlayHadDataBars + + ;
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>
2026-08-19 06:58:03 -04:00
if ( den > 0.0 & & net ! = 0.0 )
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
{
m_overlayVotedBars + + ;
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
if ( net > 0.0 ) m_overlayVotedBuy + + ;
if ( net < 0.0 ) m_overlayVotedSell + + ;
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
if ( MathAbs ( net ) > m_overlayBestNet )
m_overlayBestNet = MathAbs ( net ) ;
}
m_overlaySweptBars + + ;
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
//--- NO DATA IS NOT A VERDICT. A bar where no member had a snapshot entry (den == 0) says
//--- nothing about the vote there - deleting its arrow on that basis is how the draw/wipe
2026-08-22 00:25:52 -04:00
//--- cycle above erased whole sweeps.
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. 659638e fixed which bar was frozen, not the freezing.
* VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
different fraction of half-rebuilt caches.
THE FIX, structural rather than another patch:
1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
moment the cache is complete - and now copies it (raw signals, newest
LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
BEFORE its early return: an all-Neutral era is a snapshot worth showing,
not an absence of one. Raw signals rather than votes, so a tier re-rank
between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
snapshots; the readout's fallback chain is live-cache -> snapshot ->
dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
sub-threshold vote takes an arrow down. This alone ends the wipe half of
the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
i.e. precisely when caches are about to be wiped) to
g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
snapshot just got fresher", the only event a redraw can act on. 60s rate
limit collapses the four members' burst into one sweep. Classic-only
charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
sell)" - so "the vote leans buy" is checkable from the log instead of
inferred from arrow colours.
Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
if ( den < = 0.0 )
continue ;
2026-08-24 22:11:50 -04:00
//--- The direction policy (LONG_ONLY/SHORT_ONLY) gates the reconstruction exactly as it
//--- gates CheckOpenLong/Short live: a blocked side falls into
2026-08-22 00:25:52 -04:00
//--- the else branch below - a real verdict that deletes any standing arrow - because that
//--- trade would not have happened.
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>
2026-08-19 11:10:05 -04:00
if ( MathAbs ( net ) > = m_threshold_open & & WarriorDirectionAllows ( net > 0.0 ) )
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
{
bool isBuy = ( net > 0.0 ) ;
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
//--- SCORE BEFORE DECLUSTERING: every bar in this branch is a call the vote made under the
//--- live decision rule, whether or not NMS keeps its arrow. A bar whose label is not yet
//--- resolved (pivot pair uncommitted) is excluded from both counts, same as the replay pass.
ENUM_SIGNAL overlayTruth = Neutral ;
if ( OverlayTruthAt ( idx , overlayTruth ) )
{
m_overlayVoteFired + + ;
if ( ( isBuy & & overlayTruth = = Buy ) | | ( ! isBuy & & overlayTruth = = Sell ) )
m_overlayVoteWins + + ;
}
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>
2026-08-19 06:58:03 -04:00
//--- DECLUSTER, same three rules as the per-member arrows (PruneDirectionalClusters) and
//--- for the same reason: consecutive same-direction bars are ONE setup, and a carpet of
//--- arrows on every bar of a trend (observed 2026-08-19, "arrows on every bars") reads as
2026-08-22 00:25:52 -04:00
//--- noise, not signal.
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>
2026-08-19 06:58:03 -04:00
int lastSame = isBuy ? m_overlayNmsLastBuyIdx : m_overlayNmsLastSellIdx ;
bool sameRun = ( lastSame > = 0 & & ( lastSame - idx ) < = OVERLAY_NMS_WINDOW ) ;
if ( isBuy ) m_overlayNmsLastBuyIdx = idx ; else m_overlayNmsLastSellIdx = idx ;
if ( sameRun )
{
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>
2026-08-20 13:50:33 -04:00
WarriorDeleteSignalMark ( SIG_VOTE_PREFIX + TimeToString ( bt ) ) ;
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>
2026-08-19 06:58:03 -04:00
continue ;
}
if ( m_overlayNmsKeptIdx > = 0 & & ( m_overlayNmsKeptIdx - idx ) < = OVERLAY_NMS_WINDOW
& & m_overlayNmsKeptBuy ! = isBuy )
{
if ( MathAbs ( net ) < = m_overlayNmsKeptNet )
{
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>
2026-08-20 13:50:33 -04:00
WarriorDeleteSignalMark ( SIG_VOTE_PREFIX + TimeToString ( bt ) ) ;
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>
2026-08-19 06:58:03 -04:00
continue ; // weaker side of a flicker at one turn zone
}
//--- this bar is stronger: the earlier opposite arrow is the flicker - take it down
datetime kt = iTime ( m_symbol . Name ( ) , m_period , m_overlayNmsKeptIdx ) ;
if ( kt > 0 )
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>
2026-08-20 13:50:33 -04:00
WarriorDeleteSignalMark ( SIG_VOTE_PREFIX + TimeToString ( kt ) ) ;
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>
2026-08-19 06:58:03 -04:00
}
m_overlayNmsKeptIdx = idx ;
m_overlayNmsKeptBuy = isBuy ;
m_overlayNmsKeptNet = MathAbs ( net ) ;
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>
2026-08-19 14:11:49 -04:00
//--- Trigger price, same convention as the live mark above.
double price = iClose ( m_symbol . Name ( ) , m_period , idx ) ;
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- Marked as a reconstruction IN THE TOOLTIP, not just in a comment. Someone reading two
//--- arrows either side of the handover has to be able to tell which one is a record and
//--- which is a replay, and the chart is the only place they will look.
fix(vote): a leaf filter was dividing its own module weight back out
"Nothing on the charts." My bug, from 4858507.
Direction() is INHERITED AS-IS by every CExpertSignalCustom subclass - the
root aggregate and every leaf filter run the same function body. When I moved
the normalization from `result /= number` to `result /= weightSum` to make the
vote a weighted mean, I broke the leaf case: a leaf has no child filters, so
its numerator is exactly m_weight*ownNet and its weightSum is exactly
m_weight. Dividing there hands the parent ownNet with the module weight
divided straight back out.
The root then computed Sum(p_i)/Sum(w_i) instead of Sum(w_i*p_i)/Sum(w_i) -
inflated by 1/mean(w). At m_weight == 1 the two forms agree exactly, which is
why a fresh AI signal looked correct and the change tested fine. The moment
RankTiersFromOos() set Weight(pooled/100), or UpdateSignalsWeights() moved a
classic filter off 1.0, a vote of 60 became 60/0.4 = 150, the +-100 range
check zeroed it, and every bar voted 0. With the raw arrow layer switched off
by DrawUnfilteredSignals defaulting false, the chart had nothing left to draw.
The tell in the log is "Directional result is out of range. Setting to 0."
repeating every bar.
Only a signal that actually AGGREGATES may normalize, and in this EA that is
only ever the root - AddFilter() is called on nothing else. A leaf must return
its weighted contribution w*p, because that is what the parent's Sum(w_i)
divisor is the matching denominator for.
AND THE CHART STILL HAS A SECOND, LEGITIMATE WAY TO BE BLANK, which is the
worse problem because it is not a bug: once tiers are self-ranked to a real
holdout win rate, a weak model's vote may simply never reach Min_Vote_Open,
now defaulting to 50. That is the system correctly reporting that nothing
clears the bar - and it is INDISTINGUISHABLE on screen from a broken feature.
This codebase has already spent two days reading an unreachable gate as a
merely unmet one, so the sweep now reports its own arithmetic on completion:
bars swept, how many had any voter at all, arrows drawn, the strongest vote
seen, and the threshold it had to clear.
"0 arrows, best 41.3% vs threshold 50%" -> a finding about the models
"0 arrows, 0 bars with a voter" -> a finding about the plumbing
They need different fixes, and until now the chart said the same thing for
both.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:02:35 -04:00
m_overlayDrawn + + ;
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
//--- Recorded, not declustered here: this sweep runs NEWEST->OLDEST, so applying a cooldown
//--- inline would keep the NEWEST bar of a cluster while the live path keeps the FIRST. The
//--- prune at completion re-walks this list backwards - forward in time - so the two agree.
2026-08-27 10:34:46 -04:00
ArrayResize ( m_overlayVoteTime , m_overlayVoteCount + 1 ) ; // oldest-first, see the decl
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
ArrayResize ( m_overlayVoteBuy , m_overlayVoteCount + 1 ) ;
m_overlayVoteTime [ m_overlayVoteCount ] = bt ;
m_overlayVoteBuy [ m_overlayVoteCount ] = isBuy ;
m_overlayVoteCount + + ;
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>
2026-08-19 14:11:49 -04:00
WarriorPlotSignalLevel ( SIG_VOTE_PREFIX + TimeToString ( bt ) , bt , ( ENUM_TIMEFRAMES ) m_period , price ,
isBuy , true ,
StringFormat ( " would trade %s @ %s | confidence %.1f%% >= %.1f%% | "
" reconstructed (vote only - order validation not replayed) " ,
( isBuy ? " BUY " : " SELL " ) ,
DoubleToString ( price , m_symbol . Digits ( ) ) ,
MathAbs ( net ) , m_threshold_open ) ) ;
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
}
else
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>
2026-08-20 13:50:33 -04:00
WarriorDeleteSignalMark ( SIG_VOTE_PREFIX + TimeToString ( bt ) ) ;
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
}
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 129a0d4 is what made this findable - a blank chart
that cannot say why is indistinguishable from a broken one, and this was the
first thing it caught.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:17:25 -04:00
if ( m_overlayIndex < m_overlayStopIndex )
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
{
m_overlayPending = false ;
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
m_overlayScoreReady = true ;
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.
Two independent thinning passes over one namespace produce a union, not an
intersection.
MEASURED from the saved .votearrows files, which is what the chart actually
holds:
XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0
while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.
The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.
OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
//--- VOTE-LEVEL COOLDOWN, drawn side - RECONCILED OVER THE CHART, not over this sweep's record.
2026-08-27 10:34:46 -04:00
//---
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.
Two independent thinning passes over one namespace produce a union, not an
intersection.
MEASURED from the saved .votearrows files, which is what the chart actually
holds:
XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0
while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.
The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.
OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
//--- The record-based version was individually correct and still left clusters, because it is
//--- not the only producer. The persisted-arrow RESTORE thins its own list from its own state,
//--- this sweep thins its own list from its own state, and the LIVE gate marks the current bar
//--- from a third. Each pass spaces its OWN survivors 30 bars apart; interleaved on one chart
//--- the union can sit 1 bar apart. Measured on the saved files: 148 same-side pairs under 30
//--- bars on XTIUSD, minimum gap ZERO, while every producer's own log said it had thinned
//--- correctly.
//---
//--- So the authority is the CHART ITSELF. Whatever drew an arrow, this runs last and enforces
//--- one rule over the actual object set. OBJ_TREND only - a mark is a line AND an arrow, and
//--- the line is the canonical half (Snapshot() uses the same test for the same reason).
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
int coolWin = WarriorSignalCooldownBars ( ) ;
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.
Two independent thinning passes over one namespace produce a union, not an
intersection.
MEASURED from the saved .votearrows files, which is what the chart actually
holds:
XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0
while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.
The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.
OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
if ( coolWin > 0 )
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
{
long coolGap = ( long ) coolWin * PeriodSeconds ( ) ;
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.
Two independent thinning passes over one namespace produce a union, not an
intersection.
MEASURED from the saved .votearrows files, which is what the chart actually
holds:
XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0
while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.
The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.
OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
datetime seen [ ] ;
int found = 0 ;
int totalObj = ObjectsTotal ( 0 ) ;
ArrayResize ( seen , totalObj ) ;
for ( int oi = 0 ; oi < totalObj ; oi + + )
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
{
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.
Two independent thinning passes over one namespace produce a union, not an
intersection.
MEASURED from the saved .votearrows files, which is what the chart actually
holds:
XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0
while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.
The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.
OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
string on = ObjectName ( 0 , oi ) ;
if ( StringFind ( on , SIG_VOTE_PREFIX ) ! = 0 )
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
continue ;
fix(chart): reconcile the cooldown over the CHART, not over one producer's record
The record-based prune was individually correct and still left clusters. It is
not the only producer of a vote arrow: the persisted-arrow RESTORE thins its own
list from its own state, the overlay sweep thins its own list from its own state,
and the live gate marks the current bar from a third. Each spaces ITS OWN
survivors 30 bars apart; interleaved on one chart the union sits 1 bar apart.
Two independent thinning passes over one namespace produce a union, not an
intersection.
MEASURED from the saved .votearrows files, which is what the chart actually
holds:
XTIUSD 284 arrows, 180 gaps under 30 bars, 148 of them SAME-SIDE, min gap 0
XAUUSD 263 arrows, 177 gaps under 30 bars, 144 same-side, min gap 1
EURUSD 309 arrows, 176 gaps under 30 bars, 110 same-side, min gap 0
while every producer's own log reported it had thinned correctly. The sweep's
'drew' counter says what ONE producer drew; the chart is the union. Verifying on
that counter is what let this stand through four builds.
The authority is now the chart itself: after the sweep, walk every
SIG_VOTE_PREFIX OBJ_TREND object, sort by time, enforce one window. Whatever drew
an arrow, this runs last.
OBJ_TREND only - a mark is a line AND an arrow and the line is canonical, the
same test Snapshot() uses. Sorted first, because object order is not time order
and an unsorted forward walk yields negative gaps, which is how a prune once
deleted 272 of 273 arrows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:12:49 -04:00
if ( ObjectGetInteger ( 0 , on , OBJPROP_TYPE ) ! = OBJ_TREND )
continue ;
seen [ found + + ] = ( datetime ) ObjectGetInteger ( 0 , on , OBJPROP_TIME , 0 ) ;
}
ArrayResize ( seen , found ) ;
if ( found > 1 )
{
//--- Object order is not time order. Sorting is what makes the forward walk below mean
//--- anything - an unsorted walk yields negative gaps, and a negative gap is inside any
//--- window, which is how a prune once deleted 272 of 273 arrows.
ArraySort ( seen ) ;
datetime lastKept = 0 ;
int killed = 0 ;
for ( int v = 0 ; v < found ; v + + )
{
long gap = ( long ) ( seen [ v ] - lastKept ) ;
if ( lastKept ! = 0 & & gap > 0 & & gap < = coolGap )
{
WarriorDeleteSignalMark ( SIG_VOTE_PREFIX + TimeToString ( seen [ v ] ) ) ;
killed + + ;
continue ;
}
lastKept = seen [ v ] ;
}
if ( killed > 0 )
{
m_overlayDrawn - = killed ;
if ( m_overlayDrawn < 0 )
m_overlayDrawn = 0 ;
PrintFormat ( " Filtered view: signal cooldown (%d bars) RECONCILED the chart - removed %d "
" of %d vote arrow(s) that survived their own producer's thinning. The "
" survivors are the FIRST bar of each cluster, matching the live gate. " ,
coolWin , killed , found ) ;
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
}
}
}
2026-08-22 00:25:52 -04:00
//--- SAY WHY THE CHART LOOKS THE WAY IT DOES. So the sweep reports its own arithmetic: how
//--- many bars it looked at, how many had any voter at all, the strongest vote it saw, and
//--- the bar that vote had to clear.
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>
2026-08-19 09:36:15 -04:00
bool censusDue = VerboseMode | |
m_overlayDrawn ! = m_overlayLastLogDrawn | |
MathAbs ( m_overlayBestNet - m_overlayLastLogBest ) > = 2.0 | |
m_overlaySkippedLogs > = 9 ;
if ( ! censusDue )
m_overlaySkippedLogs + + ;
else
{
m_overlaySkippedLogs = 0 ;
m_overlayLastLogDrawn = m_overlayDrawn ;
m_overlayLastLogBest = m_overlayBestNet ;
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:58:34 -04:00
//--- THREE CAUSES, THREE SENTENCES. Reported off hadData/voted/drawn rather than off
//--- votedBars alone, which used to claim "no member has a completed era yet" for all three.
string why ;
2026-08-26 16:53:16 -04:00
if ( m_overlayHadDataBars = = 0 & & m_overlaySnapNoWeightBars > 0 )
why = StringFormat ( " %d bar(s) DID carry a member snapshot and the divisor was still zero: "
" every member that looked was ruled NO SKILL by HasDemonstratedEdge() "
" (certified precision not above its chance rate), so ReconstructionWeight() "
" returned 0 for all of them. This is a SKILL verdict, not a publication "
" fault - check each member's certified precision vs chance rate above. " ,
m_overlaySnapNoWeightBars ) ;
else
fix(training): a new best must beat the noise; the blank-chart census must name its cause
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:58:34 -04:00
if ( m_overlayHadDataBars = = 0 )
why = " NOT ONE of those bars had a single member snapshot, so the divisor was zero "
" everywhere and no vote could be formed. This is NOT a threshold outcome: it means "
" no enrolled member has published m_overlaySigSnap (filled by RankTiersFromOos at "
" pass-3 completion), or the snapshot it published holds -2.0 across this window - "
" check the era numbers above: if members are past era 1 this is an index/publication "
" fault, not a young run. " ;
else
if ( m_overlayVotedBars = = 0 )
why = StringFormat ( " %d bar(s) DID carry a snapshot and every one of them netted exactly "
" zero - the members looked and abstained. That is a calibration "
" outcome, not a drawing fault. " , m_overlayHadDataBars ) ;
else
if ( m_overlayDrawn = = 0 )
why = " The models voted but never strongly enough; this is the vote failing the "
" bar, not the drawing failing. " ;
else
why = " " ;
Print ( StringFormat ( " Filtered view: swept %d bar(s), %d had a snapshot, %d had a voter "
" (%d buy / %d sell), drew %d arrow(s). Strongest vote %.1f%% against a "
" %.1f%% threshold.%s " ,
m_overlaySweptBars , m_overlayHadDataBars , m_overlayVotedBars ,
m_overlayVotedBuy , m_overlayVotedSell ,
m_overlayDrawn , m_overlayBestNet , m_threshold_open , why ) ) ;
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>
2026-08-19 09:36:15 -04:00
}
feat(chart): reconstruct the filtered view behind the handover point
Completes the filtered view from 282b535, which only reached forward of
attach. On a multi-hour training run that is the entire time you are looking
at the chart, so the answer to "how would the whole bot have traded" was
blank exactly when it was wanted.
The sweep lives on the AGGREGATE signal, which is the only object holding
every filter. AI members contribute their CACHED per-bar decision from the
era scan - no inference re-runs, the cache already spans the chart - and the
classic ladders are replayed with EvalShift(i), the same mechanism
CSignalMETA's candidate sweep uses and exact because every classic pattern
condition anchors on StartIndex(). Combination is the live one: weighted mean
over voting filters, abstentions out of both sums, against Min_Vote_Open.
THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part
that is not obvious. Live journaling reads m_active_pattern_long/short from
the PREVIOUS Direction() call. Replaying hundreds of past bars between two
live bars leaves those slots holding whichever bar the sweep stopped on, so
the next live bar journals that pattern under the current timestamp - a
corrupted row in the very table pattern win rates are computed from, which is
now also where vote weights come from. Save/RestoreVoteState() brackets every
replayed call. CSignalMETA gets away without it only because its sweep runs
once, at the first era, before any of that state matters.
TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker
rejected an order - it has no stops level, ATR warm-up or swing-history sync
as they were at that moment - so it is an upper bound: honest about the vote,
optimistic about placement. It therefore stops dead at the handover bar,
which is latched ONCE so later rebuilds cannot creep it forward and start
overwriting real decisions with guesses, and its arrows say "reconstructed
(vote only - order validation not replayed)" in the tooltip. Someone
comparing two arrows either side of that line has to be able to tell which is
a record and which is a replay, and the chart is the only place they look.
Re-armed on any era boundary (summed era counters), because that is when the
answer changes - RankTiersFromOos has just re-derived every tier's vote weight
- and only between sweeps, so a restart cannot leave the previous pass's tail
undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on
every classic filter, which is real indicator work on the chart thread, and an
unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen.
SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside
SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should
reach the same distance or the raw and filtered views are not comparable.
Known gap: on a classic-only chart the reconstruction is built once and not
refreshed when the hourly DB ranking moves the classic weights.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
//| The truth a reconstructed vote is scored against, asked of the |
//| FIRST AI member: the swing-pivot label is a pure function of the |
//| shared chart series (ZigZag/Close/ATR over the same symbol and |
//| period every member reads), so which member answers is |
//| irrelevant - and asking exactly one keeps the sweep's cost flat |
//| in the member count. |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : OverlayTruthAt ( const int idx , ENUM_SIGNAL & truth )
{
truth = Neutral ;
int total = m_filters . Total ( ) ;
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter = = NULL | | ! filter . IsAIFilter ( ) )
continue ;
return filter . ReplayTruthAt ( idx , truth ) ;
}
return false ;
}
//+------------------------------------------------------------------+
//| One-shot harvest of the completed sweep's combined-vote score. |
//| Consuming: the ready flag drops on the first read, so one sweep |
//| can never be counted into the ensemble record twice. |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : TakeOverlayVoteScore ( long & fired , long & wins )
{
if ( ! m_overlayScoreReady )
return false ;
m_overlayScoreReady = false ;
fired = m_overlayVoteFired ;
wins = m_overlayVoteWins ;
return true ;
}
//+------------------------------------------------------------------+
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>
2026-08-19 19:31:36 -04:00
//| The bar timestamp a buffered signal carries, as a datetime. |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
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>
2026-08-19 19:31:36 -04:00
datetime SignalTime ( const SignalInfo & signal )
2026-08-09 14:51:59 -04:00
{
MqlDateTime t ;
t . year = signal . year ;
t . mon = signal . month ;
t . day = signal . day ;
t . hour = signal . hour ;
t . min = signal . minutes ;
t . sec = 0 ;
return StructToTime ( t ) ;
}
//+------------------------------------------------------------------+
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>
2026-08-19 19:31:36 -04:00
//| Order buffered signals oldest-first, ready for the DB write. |
//+------------------------------------------------------------------+
void SortSignalsByTime ( SignalInfo & signals [ ] )
{
int n = ArraySize ( signals ) ;
if ( n < 2 )
return ;
datetime keys [ ] ;
ArrayResize ( keys , n ) ;
for ( int i = 0 ; i < n ; i + + )
keys [ i ] = SignalTime ( signals [ i ] ) ;
for ( int i = 1 ; i < n ; i + + )
{
SignalInfo item = signals [ i ] ;
datetime key = keys [ i ] ;
int j = i - 1 ;
while ( j > = 0 & & keys [ j ] > key )
{
signals [ j + 1 ] = signals [ j ] ;
keys [ j + 1 ] = keys [ j ] ;
j - - ;
}
signals [ j + 1 ] = item ;
keys [ j + 1 ] = key ;
}
}
//+------------------------------------------------------------------+
2026-08-09 14:51:59 -04:00
//| Process the signal and update trades |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : ProcessBufferedSignals ( )
{
// Sort the signals array by datetime before processing
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>
2026-08-19 19:31:36 -04:00
SortSignalsByTime ( signalBuffer ) ;
2026-08-09 14:51:59 -04:00
if ( ! dbm . OpenDatabase ( ) )
{
Print ( " Failed to open database. " ) ;
return ;
}
if ( ! dbm . BeginTransaction ( ) )
{
Print ( __FUNCTION__ + " : Failed to begin database transaction, " + IntegerToString ( ArraySize ( signalBuffer ) ) + " buffered signal(s) left pending for retry next cycle. " ) ;
return ;
}
for ( int i = 0 ; i < ArraySize ( signalBuffer ) ; i + + )
{
PrintVerbose ( " Processing signal " + IntegerToString ( i + 1 ) + " of " + IntegerToString ( ArraySize ( signalBuffer ) ) ) ;
ProcessSignal ( signalBuffer [ i ] ) ;
}
if ( ! dbm . CommitTransaction ( ) )
{
Print ( __FUNCTION__ + " : Failed to commit the transaction to the database, rolling back. " + IntegerToString ( ArraySize ( signalBuffer ) ) + " buffered signal(s) left pending for retry next cycle. " ) ;
dbm . RollbackTransaction ( ) ;
return ;
}
ArrayResize ( signalBuffer , 0 ) ;
PrintVerbose ( " Signal buffer cleared after processing. " ) ;
// NOTE: does NOT close dbm here - the caller (CExpertCustom::OnTimer) opens the shared
// connection once and also calls UpdateSignalsWeights() right after this returns; closing it
// here made UpdateSignalsWeights() silently fail (BeginTransaction on a closed handle) in every
// live/demo run (IsBacktesting only skipped this close in the tester, masking the bug there).
// The opener (OnTimer) now owns closing it.
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : DeleteOldestEntry ( string tableName )
{
dbm . DeleteOldestEntry ( tableName ) ; // failure is already logged by the DB layer
}
//+------------------------------------------------------------------+
//| Register a signal in the database |
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
void CExpertSignalCustom : : RegisterSignal ( int year , int month , int day , int DOW , int hour , int minutes , string tableName , string pattern , string direction , double entryPrice , double exitPrice , string result , double netVote )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string Columns [ ] = { " year " , " month " , " day " , " dayOfWeek " , " hour " , " minutes " , " pattern " , " direction " , " entryPrice " , " exitPrice " , " result " , " netVote " } ;
string valArr [ ] = { IntegerToString ( year ) , IntegerToString ( month ) , IntegerToString ( day ) , IntegerToString ( DOW ) , IntegerToString ( hour ) , IntegerToString ( minutes ) , pattern , direction , DoubleToString ( entryPrice , Digits ( ) ) , DoubleToString ( exitPrice , Digits ( ) ) , result , DoubleToString ( netVote , 2 ) } ;
2026-08-09 14:51:59 -04:00
if ( dbm . InsertTradeRecord ( tableName , Columns , valArr ) )
{
PrintVerbose ( " Successfully registered signal in table: " + tableName ) ;
}
else
{
Print ( " Failed to register signal in table: " + tableName ) ;
}
}
//+------------------------------------------------------------------+
//| Update a trade record in the database |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : UpdateTradeRecordInDatabase ( string tableName , TradeRecord & tradeRecord )
{
string columns [ ] = { " exitPrice " , " result " } ;
string values [ ] = { DoubleToString ( tradeRecord . exitPrice , Digits ( ) ) , tradeRecord . result } ;
if ( dbm . UpdateTradeRecord ( tableName , columns , values , tradeRecord . pattern , tradeRecord . direction ) )
{
PrintVerbose ( " Successfully updated trade record in table: " + tableName ) ;
}
else
{
Print ( " Failed to update trade record in table: " + tableName + " for pattern " + tradeRecord . pattern + " and direction " + tradeRecord . direction ) ;
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : UpdateSignalsWeights ( void )
{
if ( ! dbm . BeginTransaction ( ) )
return ( false ) ;
int total = m_filters . Total ( ) ;
double sumModuleWeight = 0.0 ;
int weightedFilterCount = 0 ;
2026-08-12 18:53:04 -04:00
//--- Rows at or after 'now' can only exist in a resumed/mixed database and must not leak into
//--- weights mid-backtest; the bound is applied inside SQLite (see FetchWinLossCounts). It replaces
//--- the tester-only array trim the old full-table fetch did here, and is harmless live: a row's
//--- open time is never in the future.
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>
2026-08-19 11:37:44 -04:00
MqlDateTime brokerNow ;
TimeCurrent ( brokerNow ) ; // broker clock, matching the row stamps since dbVersion 4.0
long nowKey = SignalTimeKey ( brokerNow . year , brokerNow . mon , brokerNow . day , brokerNow . hour , brokerNow . min ) ;
2026-08-09 14:51:59 -04:00
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
//--- check pointer
if ( filter = = NULL )
continue ;
string filterID = filter . GetFilterID ( ) ;
if ( filterID = = " NULL " )
continue ;
int patternCount = filter . GetPatternCount ( ) ;
if ( patternCount < = 0 | | patternCount = = NULL )
continue ;
int totalWinRate = 0 ;
int validPatternCount = 0 ;
2026-08-22 00:25:52 -04:00
//--- POOL PASS. Counting is a pair of SQL aggregates per table (no rows materialize), so the
//--- extra pass costs the same order as the scoring pass below.
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>
2026-08-16 21:49:52 -04:00
int poolWins = 0 , poolTotal = 0 ;
for ( int j = 0 ; j < patternCount ; j + + )
{
string pPattern = PatternName ( j ) ;
int pw = 0 , pl = 0 ;
if ( dbm . FetchWinLossCounts ( PatternTableName ( filterID , pPattern , " Buy " ) , nowKey , pw , pl ) )
{
poolWins + = pw ;
poolTotal + = pw + pl ;
}
pw = 0 ;
pl = 0 ;
if ( dbm . FetchWinLossCounts ( PatternTableName ( filterID , pPattern , " Sell " ) , nowKey , pw , pl ) )
{
poolWins + = pw ;
poolTotal + = pw + pl ;
}
}
double poolPct = ( poolTotal > 0 ) ? ( 100.0 * poolWins / poolTotal ) : -1.0 ;
2026-08-22 00:25:52 -04:00
//--- One MIN_TRADES_FOR_WIN_RATE-worth of pseudo-trades: a tier measured at exactly the
//--- minimum ends up half pool / half its own evidence, and the pull halves again with every
//--- doubling of its sample.
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>
2026-08-16 21:49:52 -04:00
int poolWeight = ( poolTotal > 0 ) ? MIN_TRADES_FOR_WIN_RATE : 0 ;
2026-08-09 14:51:59 -04:00
for ( int j = 0 ; j < patternCount ; j + + )
{
2026-08-12 18:53:04 -04:00
// Aggregate outcome counts, computed inside SQLite - no rows materialize into MQL arrays,
// so this cycle's cost is flat in table size (the same fix as ProcessSignal's lookups).
2026-08-09 14:51:59 -04:00
string pattern = PatternName ( j ) ;
string tableNameBuy = PatternTableName ( filterID , pattern , " Buy " ) ;
string tableNameSell = PatternTableName ( filterID , pattern , " Sell " ) ;
2026-08-12 18:53:04 -04:00
int winsBuy = 0 , lossesBuy = 0 , winsSell = 0 , lossesSell = 0 ;
if ( ! dbm . FetchWinLossCounts ( tableNameBuy , nowKey , winsBuy , lossesBuy ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( __FUNCTION__ + " Failed to count outcomes in " + tableNameBuy ) ;
2026-08-09 14:51:59 -04:00
continue ;
}
2026-08-12 18:53:04 -04:00
if ( ! dbm . FetchWinLossCounts ( tableNameSell , nowKey , winsSell , lossesSell ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( __FUNCTION__ + " Failed to count outcomes in " + tableNameSell ) ;
2026-08-09 14:51:59 -04:00
continue ;
}
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>
2026-08-16 21:49:52 -04:00
int winRateBuy = WinRateFromCounts ( winsBuy , lossesBuy , poolPct , poolWeight ) ;
int winRateSell = WinRateFromCounts ( winsSell , lossesSell , poolPct , poolWeight ) ;
2026-08-09 14:51:59 -04:00
// Skip sides with insufficient samples instead of averaging in the sentinel
if ( winRateBuy = = NO_DATA_WIN_RATE & & winRateSell = = NO_DATA_WIN_RATE )
continue ;
int combinedWinRate = ( winRateBuy = = NO_DATA_WIN_RATE ) ? winRateSell :
( winRateSell = = NO_DATA_WIN_RATE ) ? winRateBuy :
( winRateBuy + winRateSell ) / 2 ;
if ( combinedWinRate > = 0 & & combinedWinRate < = 100 )
{
filter . ApplyPatternWeight ( j , combinedWinRate ) ;
totalWinRate + = combinedWinRate ;
validPatternCount + + ;
PrintVerbose ( " Applied " + filterID + " " + pattern + " Weight " + IntegerToString ( combinedWinRate ) ) ;
}
}
// Calculate the average win rate for valid patterns
double averageWinRate = validPatternCount > 0 ? ( totalWinRate ) / validPatternCount : 0.0 ;
// Normalize the average win rate to the range 0 to 1
double normalizedWinRate = averageWinRate / 100.0 ;
// Round the normalized win rate to the nearest 0.05
normalizedWinRate = MathRound ( normalizedWinRate * 10 ) / 10.0 ;
// Ensure the rounded value is within 0 to 1
normalizedWinRate = MathMax ( 0 , MathMin ( normalizedWinRate , 1 ) ) ;
// Apply the main weight based on the normalized and rounded win rate
double moduleWeight = normalizedWinRate ;
2026-08-22 00:25:52 -04:00
//--- ...but not over a self-ranking filter.
feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat 4858507 shipped with: the vote is a confidence percentage,
but only to the extent the pattern weights are measured. AI tier weights sat
at their designed defaults (25/50/75/100) because AI rows only ever arrive
from LIVE journaling, of which a training run produces almost none.
AND A STALE-TIER BUG THAT MADE THE EVIDENCE MEANINGLESS. The OOS scan bucketed
every scanned bar by ConfidenceTier(), which reads dPrevSignal - and
dPrevSignal is assigned in PASS 1 only, never anywhere in the OOS scan. So an
entire era's fires were bucketed by one stale, unrelated bar's confidence and
landed in a SINGLE tier. That is the "tier prec T0:72%(828) T1:n/a(0)
T2:n/a(0) T3:n/a(0)" symptom recorded on 2026-08-16 and attributed to the
calibration clamp. The clamp was real and was fixed then; this is a second,
independent cause of the identical output that survived that fix untouched -
which is why the log kept reading the same afterwards. Two causes, one symptom.
Now ConfidenceTierFor(adjSig): the bar this iteration actually scored.
WHY THIS DOES NOT WRITE ROWS TO THE SIGNAL DB, which was the obvious reading of
"fill the database during training". The user's own observation is the reason:
a classic Pattern_2 is a fixed geometric condition, so its win rate is
legitimately accumulated over years, but an AI Pattern_2 means "confidence
landed in tier 2" and tier 2 under era 100's weights is a different statement
from tier 2 under era 500's. The DB's value is ACCUMULATION, and accumulation
is exactly what is wrong here - it would average together models that no
longer exist, while colliding with the per-table row cap and mixing
measured-on-holdout outcomes into the live ledger's own tables. What the DB
actually supplies is a measured win rate per pattern, and pass 3 already
computes that on held-out bars, thousands at a time. So the model ranks itself
once per era, REPLACING rather than accumulating, which makes the weights
describe the current weights by construction.
ESTIMATOR. Not WinRateFromCounts(): it returns NO_DATA below 100 raw trades
BEFORE shrinking, which here would fire on every tier every era and hand all
four the pooled rate - the tiers could never separate and the mechanism would
be inert. Shrinkage is the answer to a small sample; a floor in front of it
means the shrinkage never runs. Instead: a Beta prior of TIER_PRIOR_EFF_N
pseudo-observations centred on the model's pooled holdout rate, counted in
EFFECTIVE observations, because overlapping triple-barrier labels mean 800 raw
fires can be worth ~12 independent ones. Rounded to the integer, not to the
decade NormalizeWinRate() uses, which would collapse the shrunk tiers back
into one number.
NO SAME-ERA CIRCULARITY, and it falls out of the ordering rather than a guard:
weights are computed at the END of era N, so the vote scored during era N was
cast with era N-1's weights. The deploy gate never grades a vote whose weights
were fitted on the bars it is scoring. Residual leakage remains - the same OOS
bars each era under a different model - and is stated in the code rather than
papered over.
Both DB clobber paths are closed: ApplyPatternWeight() declines once
self-ranked, and UpdateSignalsWeights()' filter.Weight() call is guarded by
SelfRanked() - guarding only the tiers would have let the hourly ranking pass
undo half the self-ranking.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:00:32 -04:00
if ( moduleWeight > 0 & & moduleWeight < = 1 & & ! filter . SelfRanked ( ) )
2026-08-09 14:51:59 -04:00
{
filter . Weight ( moduleWeight ) ;
PrintVerbose ( " Applied " + filterID + " Main Weight " + DoubleToString ( moduleWeight , 2 ) ) ;
}
if ( validPatternCount > 0 )
{
sumModuleWeight + = normalizedWinRate ;
weightedFilterCount + + ;
}
}
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
// Average DB win-rate across the filters that had enough samples to produce one. RECORDED ONLY -
// it is journalled per trade and never read back into a decision (the Confidence_Source input that
// let it size stops and lots was removed 2026-08-25). Note the shape it actually has: it is a mean
// of pattern win rates used for RANKING filters against one another, which is not the same
// quantity as "the probability this particular trade wins", and was never validated as one.
2026-08-09 14:51:59 -04:00
m_dbConfidence = weightedFilterCount > 0 ? sumModuleWeight / weightedFilterCount : 0.0 ;
if ( dbm . CommitTransaction ( ) )
return true ;
else
return ( false ) ;
}
//+------------------------------------------------------------------+
2026-08-12 18:53:04 -04:00
//| Win rate from SQL-side outcome counts (see FetchWinLossCounts) |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
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>
2026-08-16 21:49:52 -04:00
int CExpertSignalCustom : : WinRateFromCounts ( const int wins , const int losses , const double priorPct ,
const int priorWeight )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
int totalTrades = wins + losses ;
2026-08-09 14:51:59 -04:00
if ( totalTrades < MIN_TRADES_FOR_WIN_RATE )
return NO_DATA_WIN_RATE ;
2026-08-22 00:25:52 -04:00
//--- Shrunk toward the pool this ladder belongs to - the caller supplies it, and a caller with
//--- no pool passes priorWeight 0 for the raw ratio.
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>
2026-08-19 23:47:06 -04:00
return NormalizeWinRate ( ShrunkRatePct ( ( double ) wins , ( double ) totalTrades , priorPct , ( double ) priorWeight ) ) ;
2026-08-09 14:51:59 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int CExpertSignalCustom : : NormalizeWinRate ( double winRate )
{
return ( int ) MathRound ( winRate / 10 ) * 10 ; // Round to the nearest 10
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : OnTickHandler ( void )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
//--- WHOLE TREE, not just the voters: OnTickHandler is what drives each AI signal's training,
//--- and a gate left out of it silently stops learning.
int total = ChildSignalCount ( ) ;
2026-08-09 14:51:59 -04:00
for ( int i = 0 ; i < total ; i + + )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
CExpertSignalCustom * filter = ChildSignalAt ( i ) ;
2026-08-09 14:51:59 -04:00
//--- check pointer
if ( filter = = NULL )
continue ;
2026-08-22 00:25:52 -04:00
//--- NO GetFilterID() == "NULL" TEST HERE any more. CSignalNewsFilter, CSignalSessionFilter
//--- and CSignalRiskGuard never set an id, so all three were silently skipped here and in
//--- OnChartEventHandler below.
2026-08-09 14:51:59 -04:00
filter . OnTickHandler ( ) ;
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : OnChartEventHandler ( const int id ,
const long & lparam ,
const double & dparam ,
const string & sparam )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
int total = ChildSignalCount ( ) ; // whole tree - a gate has panel controls like any signal
2026-08-09 14:51:59 -04:00
for ( int i = 0 ; i < total ; i + + )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
CExpertSignalCustom * filter = ChildSignalAt ( i ) ;
2026-08-09 14:51:59 -04:00
//--- check pointer
if ( filter = = NULL )
continue ;
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
//--- no id test - see OnTickHandler above.
2026-08-09 14:51:59 -04:00
filter . OnChartEventHandler ( id , lparam , dparam , sparam ) ;
}
}
//+------------------------------------------------------------------+
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
//| Hands a panel command to this signal and every filter under it, |
//| and reports how many acted on it. |
//+------------------------------------------------------------------+
int CExpertSignalCustom : : DispatchSignalCommand ( const ENUM_SIGNAL_COMMAND cmd )
{
int acted = OnSignalCommand ( cmd ) ? 1 : 0 ;
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
int total = ChildSignalCount ( ) ; // whole tree - a panel command must reach a gate too
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
for ( int i = 0 ; i < total ; i + + )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
CExpertSignalCustom * filter = ChildSignalAt ( i ) ;
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
if ( filter = = NULL )
continue ;
acted + = filter . DispatchSignalCommand ( cmd ) ;
}
return acted ;
}
//+------------------------------------------------------------------+
//| How many signals in this subtree carry a given trait. |
//+------------------------------------------------------------------+
int CExpertSignalCustom : : CountSignalTrait ( const ENUM_SIGNAL_TRAIT trait )
{
int n = HasSignalTrait ( trait ) ? 1 : 0 ;
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
int total = ChildSignalCount ( ) ; // the panel acts on the whole tree, so it must count it
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
for ( int i = 0 ; i < total ; i + + )
{
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 (d81ec15) had exactly this shape.
CExpertSignalCustom::IsVotingSignal() is the predicate, false for a meta
target. AddFilter() ROUTES on it into a second owned list, m_gates, so
the EA's init code stays one uniform AddFilterToSignal() call per signal
and the invariant is enforced in one place instead of re-checked by
every reader.
THE TRAP, and it is why this is not just a deletion: m_filters is not
only the voting list, it is also how a signal reaches its children for
INDICATORS, TICKS, PANEL COMMANDS, CHART EVENTS and TRAIT COUNTS.
OnTickHandler in particular is what drives each AI signal's training - a
gate dropped from it silently stops learning. So the tree is now split
by purpose:
m_filters (voting) Direction, HistoricalNetVote,
RefreshVoteReadout, vote rollback,
UpdateSignalsWeights (pattern/DB weights)
ChildSignalAt (whole tree) InitIndicators, OnTickHandler,
OnChartEventHandler, DispatchSignalCommand,
CountSignalTrait
and the IsMetaTarget() special case in VoteCapableWeight() is deleted -
the structure now guarantees what it was hand-checking.
META was already added last, so no filter's m_ignore/m_invert bit index
moves.
Not done here: removing META outright. It is default-off and has never
shown an operating point clearing break-even, so the case for deleting
it is real - but that is a feature decision, not a refactor, and it is
offered separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:03:48 -04:00
CExpertSignalCustom * filter = ChildSignalAt ( i ) ;
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 (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
if ( filter = = NULL )
continue ;
n + = filter . CountSignalTrait ( trait ) ;
}
return n ;
}
//+------------------------------------------------------------------+