Warrior_EA/Expert/ExpertSignalCustom.mqh

2367 lines
125 KiB
MQL5
Raw Permalink Normal View History

feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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"
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
//--- Enumerations
#include "..\Enumerations\GlobalEnums.mqh"
refactor(meta): the veto is a gate, not a virtual every signal carries Since S3 (f64e0f8) the meta head casts no vote - it scores an entry the consensus already cleared and vetoes the ones under the cost-adjusted break-even. The code still said otherwise. LiveMetaGate() was a virtual on CExpertSignalCustom, so MA, RSI, MACD, Ichimoku, the four direction nets, the session and news filters and the risk guard each carried a meta-gate method they had no business having; one class implemented it and a dozen inherited it. The trading pipeline held the gate as a CExpertSignalCustom* - a signal pointer, with a signal's two hundred other methods reachable from the entry path. Expert\Trading\MetaGate.mqh now owns the abstraction: CMetaGate one pure virtual, Evaluate(), and the two static readings of a verdict (Blocks / Scored) META_GATE_* names for the four codes the three call sites used to spell as bare 0/1/2 and test three different ways (`< 0` here, `== 2` there, `else` for the rest). Codes unchanged; only ONE of them blocks, and that asymmetry is now stated where it lives. SMetaGateTelemetry the five m_metaGate* members that were on the AI signal base - inherited by every direction model, meaningful for none of them. One lifetime, one writer, one object; the arm latch and the two counters are a set that clears together. g_warriorMetaGate is a CMetaGate*. MQL5's single inheritance means the head cannot also BE one (it already extends the AI base for the net, the era loop, the feature windows, the label caches and persistence), so it owns a bound CMetaGateAdapter and hands that out - the same shape CTrainingDataView uses for the same reason. LiveMetaGate() is gone from the signal base. Behaviour unchanged: same codes, same thresholds, same fail-open doctrine, same live-only telemetry rule. The adapter fails open when unbound, on that same doctrine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:33:47 -04:00
//--- The meta-labeling VETO, as its own abstraction rather than a virtual every signal carries.
#include "Trading\MetaGate.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
//+------------------------------------------------------------------+
//| 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
//--- user request). The LINE is a short horizontal segment at the trigger price - the precise
//--- entry/ exit level, readable only zoomed in.
#define WARRIOR_SIG_BUY_COLOR clrDodgerBlue
#define WARRIOR_SIG_SELL_COLOR clrRed
//--- THE COLOUR IS THE DIRECTION ENCODING, not decoration - a signal line carries no arrow code, so
//--- SaveChartSignals recovers buy-vs-sell by comparing against WARRIOR_SIG_BUY_COLOR. 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
//--- 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
//+------------------------------------------------------------------+
//| 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);
//--- 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
}
//--- THE VOTE READOUT's own object namespace.
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(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
//--- INTELLIGENT trade direction - the measured drift verdict, written by the label-cache prebuild
//--- (Expert\AIBase\Labels.mqh, see the verdict block there for the statistics). BOTH until
//--- measured - the safe state, and the permanent state on classic-only charts, which never build a
//--- label cache.
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 g_warriorDriftVerdict = BOTH;
bool g_warriorDriftMeasured = false;
//--- The one resolution point for the Trade direction input: INTELLIGENT defers to the measured
//--- verdict, everything else is what it always was. Every gate - live entry, reconstruction,
//--- HUD verdict - resolves through here so they cannot drift apart.
TRADING_DIRECTION WarriorEffectiveDirection(void)
{
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
return (tradingdirection == DIRECTION_INTELLIGENT) ? g_warriorDriftVerdict : 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);
}
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. |
//| |
//| 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. |
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:
//--- "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;
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -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
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
//--- Underlying-int sentinel for the "Intelligent" SL/TP modes (STOP_LOSS_MODE::SL_INTELLIGENT /
//--- TAKE_PROFIT_MODE::TP_INTELLIGENT, both -1 in Enumerations\InputEnums.mqh).
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
#define SL_INTELLIGENT_MODE (-1)
#define TP_INTELLIGENT_MODE (-1)
//--- 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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
#define SL_INTELLIGENT_BASE_MULT 3.0
#define TP_INTELLIGENT_BASE_RR 2.5
#define AI_SL_TIGHTEN_FACTOR 0.3
#define AI_TP_WIDEN_FACTOR 1.0
//--- ENTRY_MULTIPLIER "Intelligent"/"Prev swing" sentinels (ENTRY_INTELLIGENT/ENTRY_PREV_SWING in
//--- Enumerations\InputEnums.mqh, -100/-101), kept as local macros for the same include-order
//--- independence as the SL/TP sentinels above.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
#define ENTRY_INTELLIGENT_MODE (-100)
#define ENTRY_PREV_SWING_MODE (-101)
#define ENTRY_INTELLIGENT_BASE_MULT 2.0
//
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
string PatternName(int patternIndex) { return "Pattern_" + IntegerToString(patternIndex); }
SignalInfo signalBuffer[];
protected:
//--- 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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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).
//--- The DB journaling reads ONLY the per-side slots.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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"
//--- This filter's own net vote, LongCondition() - ShortCondition(), in pattern-weight units
//--- 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;
//--- 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;
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
//--- NON-VOTING CHILDREN, owned exactly as m_filters owns its own (CArrayObj frees on
//--- destruct). Kept out of the voting list so no vote consumer needs a special case - every
//--- one of those was a bug waiting, and the replay's divisor was one.
CArrayObj m_gates;
//--- THE META VETO THIS TREE ANSWERS TO (S3). Held by the ROOT signal - the one CExpert calls
//--- CheckOpenLong/Short on - and borrowed by its children through MetaGate() below. This was a
//--- file-scope global; a signal tree owning its own gate is not only tidier, it retires the
//--- stale-pointer hazard the global needed hand-clearing for (the root is new'd fresh at every
//--- re-init, so nothing can survive one).
CMetaGate *m_metaGate;
//--- Who adopted this signal, NULL for a root. AddFilter() sets it. A raw back-pointer is safe
//--- here for the same reason the gate adapter's owner pointer is: m_filters/m_gates free their
//--- children, so a parent always outlives them.
CExpertSignalCustom *m_parentSignal;
//--- HISTORICAL FILTERED-OVERLAY sweep state (see AdvanceFilteredOverlay).
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;
int m_overlayIndex; // next bar index to process, walking newest -> oldest
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
//--- 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;
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
//--- 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(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;
feat(hud): per-member neuron lines + a vote label that moves as the nets learn Both 2026-08-19 reports were the same staleness: every source behind the label was an ERA artifact (live cache refills at pass-3 completion, the snapshot copies once per era, dPrevSignal is the frozen purge-band edge bar) - so the readout stepped at era cadence at best, stayed glued to one direction, and lagged the era counter. DisplayInference(): throttled (4s, 1s across an era boundary), SIDE-EFFECT-FREE forward of the current decision bar (window ending on bar 1, same question the live path asks) through the LEARNER net. Batch-norm running stats are bracketed frozen/RESTORED via the new CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore, not unfreeze, because a display tick can land between pass-3 chunks whose whole scan holds them frozen. Writes nothing a trading or training path reads (dPrevSignal, NMS state, tallies, watermarks all untouched; RefreshLatestSignal is not reusable here precisely because it writes all of them). LSTM safe by construction: h/c zeroed per forward. ProspectiveVote() reads the fresh forward as its FIRST source; the era-artifact chain becomes the fallback (meta head, warm-up, window holes). DisplayHudLine(): the reference library's training label, per ensemble member - name, output activations (softmax probs or raw scalar), the decision, its weighted vote (the exact consensus numerator term), era, recent average error, "(trn)" while not vote-capable. Rendered under the vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines are telemetry, not tradable readings), coloured by the member's own direction in muted tones - the vote line's strict green-only-when-it-would-trade rule is untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00
//--- How many per-member HUD lines are currently on the chart, so a shrink (member disabled,
//--- filters rebuilt) deletes the orphans instead of leaving a frozen line from a model that
//--- no longer exists - the exact stale-display failure the snapshot rule exists to prevent.
int m_hudMemberLines;
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;
int m_maxTableRows; // per-table row cap, from the DB_MaxRowsPerTable input
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
int m_sl_mode; // STOP_LOSS_MODE int: >0 = fixed ATR multiple beyond swing; SL_INTELLIGENT(-1) = AI-confidence scaled
int m_tp_mode; // TAKE_PROFIT_MODE int: >0 = fixed ATR multiple from entry; TP_INTELLIGENT(-1) = AI-confidence scaled
int m_confidence_source; // CONFIDENCE_SOURCE underlying int (0=AI, 1=DB, 2=Blended)
//--- 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.
bool m_holdToBarrier;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
double m_dbConfidence; // last average normalized DB win-rate across active filters
//--- Direction()'s per-second aggregation state. The window key is a full timestamp (broker
//--- clock since 2026-08-19), NOT MqlDateTime.sec.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
public:
CExpertSignalCustom(void);
~CExpertSignalCustom(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
//--- DOES THIS SIGNAL CAST VOTES? A gate (the meta head) answers no: its Long/ShortCondition
//--- are structurally 0 and it exists to veto entries, not to have an opinion on direction.
//--- AddFilter() routes on this, so "only true signalers are filters" is enforced in one place
//--- rather than re-checked by every consumer of the list.
virtual bool IsVotingSignal(void) const { return true; }
//--- ADOPTION. AddFilter() sets this on every child it takes, voter or gate, so a child can
//--- reach the tree it belongs to. That is how an AI filter finds the root's meta gate at era-
//--- verdict time: it sits deep in the training code and has no other route up.
void SetParentSignal(CExpertSignalCustom *parent) { m_parentSignal = parent; }
//--- The meta veto, installed on the root once the meta head exists (see Warrior_EA's OnInit).
void SetMetaGate(CMetaGate *gate) { m_metaGate = gate; }
CMetaGate *OwnMetaGate(void) const { return m_metaGate; }
//--- THE GATE THIS SIGNAL ANSWERS TO: its own if it is a root, its parent's if it was adopted.
//--- One level, because AddFilterToSignal is only ever called on the root; a deeper nesting
//--- would answer NULL, which on this path means fail-open - the doctrine everywhere else too.
CMetaGate *MetaGate(void) const
{
if(CheckPointer(m_metaGate) != POINTER_INVALID)
return m_metaGate;
return (CheckPointer(m_parentSignal) != POINTER_INVALID) ? m_parentSignal.OwnMetaGate() : NULL;
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
virtual bool AddFilter(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
//--- THE ADMINISTRATIVE TREE: every child this node owns, voter or not. m_filters is the VOTING
//--- list; indicators, ticks, panel commands and trait counts must reach the whole tree, and a
//--- non-voter that is missing from them silently stops training or stops answering the panel.
int ChildSignalCount(void) const { return m_filters.Total() + m_gates.Total(); }
CExpertSignalCustom *ChildSignalAt(const int i)
{
int nf = m_filters.Total();
return (CExpertSignalCustom *)((i < nf) ? m_filters.At(i) : m_gates.At(i - nf));
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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; }
void ConfidenceSource(int value) { m_confidence_source = value; }
void HoldToBarrier(bool value) { m_holdToBarrier = value; }
bool HoldToBarrier(void) const { return m_holdToBarrier; }
//--- HISTORICAL EVALUATION SHIFT (meta-labeling candidate sweep). Non-zero only inside
//--- CSignalMETA's corpus sweep; 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; }
//--- CONFIGURED EVALUATION BAR (Classic_Shift input, classic votes only). The sweep above still
//--- wins when it is active.
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));
}
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
//--- Deep-history readiness for the sweep: the price series and each signal's own indicator
//--- buffers default to a shallow depth, so reads at bar 40,000 would fail. Overridden per signal
//--- class to also resize its indicator; the base handles the shared price series.
virtual bool SweepPrepare(const int bars)
{
bool ok = true;
if(CheckPointer(m_open) != POINTER_INVALID)
{
ok = m_open.BufferResize(bars) && ok;
m_open.Refresh(-1);
}
if(CheckPointer(m_high) != POINTER_INVALID)
{
ok = m_high.BufferResize(bars) && ok;
m_high.Refresh(-1);
}
if(CheckPointer(m_low) != POINTER_INVALID)
{
ok = m_low.BufferResize(bars) && ok;
m_low.Refresh(-1);
}
if(CheckPointer(m_close) != POINTER_INVALID)
{
ok = m_close.BufferResize(bars) && ok;
m_close.Refresh(-1);
}
return ok;
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
// 0.0 = no AI confidence available (pure rule-based); overridden in
// CExpertSignalAIBase to return the live signal's confidence in [0,1].
virtual double AIConfidence(void) { return 0.0; }
// Signed version of AIConfidence: sign gives direction (+ buy, - sell), used for
// AI-driven early exit. 0.0 = no AI filter (base rule-based class never exits early).
virtual double SignedAIConfidence(void) { return 0.0; }
//--- 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).
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
double LiveSignedConfidence(void);
2026-08-19 18:55:36 -04:00
// Combines AIConfidence()/m_dbConfidence per m_confidence_source into a single 0..1
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
// magnitude, used to scale SL/TP and (Intelligent MM) lot size.
double EffectiveConfidence(void);
virtual void ApplyPatternWeight(int patternNumber, int weight) {};
void ID(string id) { m_id = id; }
virtual string GetFilterID(void) { return m_id; };
//--- 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; }
//--- 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
//--- 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 -
//--- 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(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(hud): per-member neuron lines + a vote label that moves as the nets learn Both 2026-08-19 reports were the same staleness: every source behind the label was an ERA artifact (live cache refills at pass-3 completion, the snapshot copies once per era, dPrevSignal is the frozen purge-band edge bar) - so the readout stepped at era cadence at best, stayed glued to one direction, and lagged the era counter. DisplayInference(): throttled (4s, 1s across an era boundary), SIDE-EFFECT-FREE forward of the current decision bar (window ending on bar 1, same question the live path asks) through the LEARNER net. Batch-norm running stats are bracketed frozen/RESTORED via the new CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore, not unfreeze, because a display tick can land between pass-3 chunks whose whole scan holds them frozen. Writes nothing a trading or training path reads (dPrevSignal, NMS state, tallies, watermarks all untouched; RefreshLatestSignal is not reusable here precisely because it writes all of them). LSTM safe by construction: h/c zeroed per forward. ProspectiveVote() reads the fresh forward as its FIRST source; the era-artifact chain becomes the fallback (meta head, warm-up, window holes). DisplayHudLine(): the reference library's training label, per ensemble member - name, output activations (softmax probs or raw scalar), the decision, its weighted vote (the exact consensus numerator term), era, recent average error, "(trn)" while not vote-capable. Rendered under the vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines are telemetry, not tradable readings), coloured by the member's own direction in muted tones - the vote line's strict green-only-when-it-would-trade rule is untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00
//--- One HUD line describing this member's CURRENT raw opinion - the output neurons, the
//--- decision they resolve to, its weighted vote, era and training error. Empty string = no
//--- line; only AI members override.
feat(hud): per-member neuron lines + a vote label that moves as the nets learn Both 2026-08-19 reports were the same staleness: every source behind the label was an ERA artifact (live cache refills at pass-3 completion, the snapshot copies once per era, dPrevSignal is the frozen purge-band edge bar) - so the readout stepped at era cadence at best, stayed glued to one direction, and lagged the era counter. DisplayInference(): throttled (4s, 1s across an era boundary), SIDE-EFFECT-FREE forward of the current decision bar (window ending on bar 1, same question the live path asks) through the LEARNER net. Batch-norm running stats are bracketed frozen/RESTORED via the new CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore, not unfreeze, because a display tick can land between pass-3 chunks whose whole scan holds them frozen. Writes nothing a trading or training path reads (dPrevSignal, NMS state, tallies, watermarks all untouched; RefreshLatestSignal is not reusable here precisely because it writes all of them). LSTM safe by construction: h/c zeroed per forward. ProspectiveVote() reads the fresh forward as its FIRST source; the era-artifact chain becomes the fallback (meta head, warm-up, window holes). DisplayHudLine(): the reference library's training label, per ensemble member - name, output activations (softmax probs or raw scalar), the decision, its weighted vote (the exact consensus numerator term), era, recent average error, "(trn)" while not vote-capable. Rendered under the vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines are telemetry, not tradable readings), coloured by the member's own direction in muted tones - the vote line's strict green-only-when-it-would-trade rule is untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00
virtual string DisplayHudLine(void) { return ""; }
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
//--- 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; }
//--- 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.
double HistoricalNetVote(const int idx, double &capableOut);
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);
bool FilteredOverlayPending(void) const { return m_overlayPending; }
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);
//--- 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
//--- 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
//--- entry the triple-barrier label assumes (see TripleBarrierLabel). The spread the label
//--- charges is smaller than a chart pixel at normal zoom, so it is priced but not drawn.
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
//--- 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; }
//--- 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; }
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
virtual int GetPatternCount(void) { return m_pattern_count; };
//--- 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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
virtual double Direction(void) override;
//--- Vote lifecycle hooks, for filters whose LongCondition()/ShortCondition() consume one-shot
//--- state when they fire. Base = no-op.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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; };
void MaxTableRows(int value) { m_maxTableRows = MathMax(1, value); };
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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),
m_metaGate(NULL),
m_parentSignal(NULL),
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),
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(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),
feat(hud): per-member neuron lines + a vote label that moves as the nets learn Both 2026-08-19 reports were the same staleness: every source behind the label was an ERA artifact (live cache refills at pass-3 completion, the snapshot copies once per era, dPrevSignal is the frozen purge-band edge bar) - so the readout stepped at era cadence at best, stayed glued to one direction, and lagged the era counter. DisplayInference(): throttled (4s, 1s across an era boundary), SIDE-EFFECT-FREE forward of the current decision bar (window ending on bar 1, same question the live path asks) through the LEARNER net. Batch-norm running stats are bracketed frozen/RESTORED via the new CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore, not unfreeze, because a display tick can land between pass-3 chunks whose whole scan holds them frozen. Writes nothing a trading or training path reads (dPrevSignal, NMS state, tallies, watermarks all untouched; RefreshLatestSignal is not reusable here precisely because it writes all of them). LSTM safe by construction: h/c zeroed per forward. ProspectiveVote() reads the fresh forward as its FIRST source; the era-artifact chain becomes the fallback (meta head, warm-up, window holes). DisplayHudLine(): the reference library's training label, per ensemble member - name, output activations (softmax probs or raw scalar), the decision, its weighted vote (the exact consensus numerator term), era, recent average error, "(trn)" while not vote-capable. Rendered under the vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines are telemetry, not tradable readings), coloured by the member's own direction in muted tones - the vote line's strict green-only-when-it-would-trade rule is untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00
m_hudMemberLines(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),
m_maxTableRows(MAX_TABLE_ROWS),
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
m_confidence_source(0),
m_holdToBarrier(false),
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
{
}
//+------------------------------------------------------------------+
//| Combine AI/DB confidence per the configured Confidence_Source |
//+------------------------------------------------------------------+
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;
//--- 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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
}
double CExpertSignalCustom::EffectiveConfidence(void)
{
g_AISignedConfidence = LiveSignedConfidence();
g_DBConfidence = m_dbConfidence;
return CombinedConfidence(m_confidence_source);
}
//+------------------------------------------------------------------+
//| 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();
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
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
//--- ONLY TRUE SIGNALERS GO IN THE VOTING LIST. A non-voter still needs everything else a
//--- signal needs - indicators, ticks, panel commands, traits - so it is adopted into m_gates
//--- and the administrative walks pick it up from there. Routing here means the EA's init code
//--- stays one uniform AddFilterToSignal() call per signal.
CExpertSignalCustom *asCustom = dynamic_cast<CExpertSignalCustom*>(filter);
//--- Adoption is adoption: a child knows the tree it belongs to whichever list it lands in.
//--- That back-pointer is how an AI filter reaches the root's meta gate at era-verdict time -
//--- it is deep in the training code, has no other route up, and a file-scope global used to be
//--- that route.
if(asCustom != NULL)
asCustom.SetParentSignal(GetPointer(this));
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
if(asCustom != NULL && !asCustom.IsVotingSignal())
{
if(!m_gates.Add(filter))
return false;
filter.EveryTick(m_every_tick);
filter.Magic(m_magic);
return true;
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
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;
}
//+------------------------------------------------------------------+
//| 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. |
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE CExpertSignalCustom::ResolveOrderType(bool isLong, double price)
{
if(price <= 0.0)
return(isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
double stops = TCStopsLevel(m_symbol.Name());
if(isLong)
{
double ask = m_symbol.Ask();
if(price > ask + stops)
return(ORDER_TYPE_BUY_STOP);
if(price < ask - stops)
return(ORDER_TYPE_BUY_LIMIT);
return(ORDER_TYPE_BUY);
}
double bid = m_symbol.Bid();
if(price > bid + stops)
return(ORDER_TYPE_SELL_LIMIT);
if(price < bid - stops)
return(ORDER_TYPE_SELL_STOP);
return(ORDER_TYPE_SELL);
}
//+------------------------------------------------------------------+
//| 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);
//--- 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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
// Refresh the confidence bridge every tick regardless of SL/TP mode, so Intelligent MM
// (Money\MoneyIntelligent.mqh), the intelligent trailing (Trailing\TrailingIntelligent.mqh), and
// intelligent entry below all see a fresh value even when SL/TP are left on fixed-ATR presets.
double confidence = EffectiveConfidence();
if(!MathIsValidNumber(confidence))
confidence = 0.0;
//--- --- Entry price. Offsets are measured from the CURRENT price (base_price = bid/ask), except
//--- ENTRY_PREV_SWING which anchors to the recent swing.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 if(entryMode == ENTRY_INTELLIGENT_MODE)
{
// Deep limit pullback when unsure, shrinking to a market fill as confidence -> 1.
double pull = ENTRY_INTELLIGENT_BASE_MULT * (1.0 - confidence) * atr;
price = m_symbol.NormalizePrice(isLong ? (base_price - pull) : (base_price + pull));
}
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));
//--- --- Stop loss: always ENTRY-anchored, a straight ATR multiple below (long) / above (short)
//--- the entry price. SL_ATR_* use that multiple directly; SL_INTELLIGENT starts at
//--- SL_INTELLIGENT_BASE_MULT and tightens as confidence rises.
//--- MEASURED GEOMETRY OVERRIDE (2026-08-09). A trade with any other geometry is a different
//--- bet, one the gate never graded - the model was being graded on one game and paid on
//--- another.
fix: live trades now use the geometry the gate certifies; perf: BN kernels Three changes, one theme: the trade placed, the trade graded, and the trade computed are now the same trade. 1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier pair reached the LABELS only - OpenParams still placed orders at the enum geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before 3.33*ATR above break-even" about trades the EA never placed. Published via g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick contract as the confidence globals, because OpenParams runs on the root signal which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at era 0, and the .cfg adoption a deployed model takes. Overrides both legs and both Intelligent modes - the certificate is exact or it is nothing. TP is ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot reshape the certified target. 2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl - forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a line-for-line transcription of the host implementation (NormalizeHost / HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the exact moment-write ordering. The host copies remain the runtime for the DLL and pure-MQL5 tiers and the reference the kernels must match. Because this box has no OpenCL platform, the safety story is layered: - shim validation: kernels compiled as C and driven against a fp64 host transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff 0.132 vs tolerance 1.0 - in-situ self-check: each kernel is compared against its host twin ON FIRST USE on the real device (SelfCheckBn*), covering what the shim cannot - arg indices and buffer bindings. Any disagreement resyncs from the good copy, latches all BN kernels off process-wide, and training continues host-side. A transcription bug costs a warning and some speed, never a poisoned .nnw. - sync discipline: BatchOptions is now a CBufferDouble with explicit authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull read-only; restores/loads/resets push; a mid-batch handover drains the device gamma/beta accumulator into the host arrays so no sample is lost. 3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at init (one warning instead of warning + failed Execute). Build tag bumped to win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five binary-changing commits. Both build variants compile 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
bool useDerivedGeometry = (g_DerivedSlAtrMult > 0.0 && g_DerivedTpAtrMult > 0.0);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
double slMultiplier;
fix: live trades now use the geometry the gate certifies; perf: BN kernels Three changes, one theme: the trade placed, the trade graded, and the trade computed are now the same trade. 1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier pair reached the LABELS only - OpenParams still placed orders at the enum geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before 3.33*ATR above break-even" about trades the EA never placed. Published via g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick contract as the confidence globals, because OpenParams runs on the root signal which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at era 0, and the .cfg adoption a deployed model takes. Overrides both legs and both Intelligent modes - the certificate is exact or it is nothing. TP is ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot reshape the certified target. 2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl - forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a line-for-line transcription of the host implementation (NormalizeHost / HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the exact moment-write ordering. The host copies remain the runtime for the DLL and pure-MQL5 tiers and the reference the kernels must match. Because this box has no OpenCL platform, the safety story is layered: - shim validation: kernels compiled as C and driven against a fp64 host transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff 0.132 vs tolerance 1.0 - in-situ self-check: each kernel is compared against its host twin ON FIRST USE on the real device (SelfCheckBn*), covering what the shim cannot - arg indices and buffer bindings. Any disagreement resyncs from the good copy, latches all BN kernels off process-wide, and training continues host-side. A transcription bug costs a warning and some speed, never a poisoned .nnw. - sync discipline: BatchOptions is now a CBufferDouble with explicit authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull read-only; restores/loads/resets push; a mid-batch handover drains the device gamma/beta accumulator into the host arrays so no sample is lost. 3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at init (one warning instead of warning + failed Execute). Build tag bumped to win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five binary-changing commits. Both build variants compile 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if(useDerivedGeometry)
slMultiplier = g_DerivedSlAtrMult;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
else
fix: live trades now use the geometry the gate certifies; perf: BN kernels Three changes, one theme: the trade placed, the trade graded, and the trade computed are now the same trade. 1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier pair reached the LABELS only - OpenParams still placed orders at the enum geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before 3.33*ATR above break-even" about trades the EA never placed. Published via g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick contract as the confidence globals, because OpenParams runs on the root signal which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at era 0, and the .cfg adoption a deployed model takes. Overrides both legs and both Intelligent modes - the certificate is exact or it is nothing. TP is ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot reshape the certified target. 2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl - forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a line-for-line transcription of the host implementation (NormalizeHost / HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the exact moment-write ordering. The host copies remain the runtime for the DLL and pure-MQL5 tiers and the reference the kernels must match. Because this box has no OpenCL platform, the safety story is layered: - shim validation: kernels compiled as C and driven against a fp64 host transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff 0.132 vs tolerance 1.0 - in-situ self-check: each kernel is compared against its host twin ON FIRST USE on the real device (SelfCheckBn*), covering what the shim cannot - arg indices and buffer bindings. Any disagreement resyncs from the good copy, latches all BN kernels off process-wide, and training continues host-side. A transcription bug costs a warning and some speed, never a poisoned .nnw. - sync discipline: BatchOptions is now a CBufferDouble with explicit authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull read-only; restores/loads/resets push; a mid-batch handover drains the device gamma/beta accumulator into the host arrays so no sample is lost. 3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at init (one warning instead of warning + failed Execute). Build tag bumped to win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five binary-changing commits. Both build variants compile 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if(m_sl_mode == SL_INTELLIGENT_MODE)
slMultiplier = SL_INTELLIGENT_BASE_MULT * (1.0 - AI_SL_TIGHTEN_FACTOR * confidence);
else
slMultiplier = (double)m_sl_mode;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
sl = isLong ? m_symbol.NormalizePrice(price - slMultiplier * atr)
: m_symbol.NormalizePrice(price + slMultiplier * atr);
//--- Enforce a hard minimum SL distance from entry (broker stop-level / sanity floor).
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
double risk = fabs(price - sl);
//--- --- Take profit: TP_ATR_* are an ATR multiple FROM THE ENTRY PRICE; TP_INTELLIGENT is a
//--- multiple of THIS TRADE'S OWN RISK, widening with confidence. Min RR (below) only rejects,
//--- never reshapes either.
fix: live trades now use the geometry the gate certifies; perf: BN kernels Three changes, one theme: the trade placed, the trade graded, and the trade computed are now the same trade. 1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier pair reached the LABELS only - OpenParams still placed orders at the enum geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before 3.33*ATR above break-even" about trades the EA never placed. Published via g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick contract as the confidence globals, because OpenParams runs on the root signal which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at era 0, and the .cfg adoption a deployed model takes. Overrides both legs and both Intelligent modes - the certificate is exact or it is nothing. TP is ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot reshape the certified target. 2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl - forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a line-for-line transcription of the host implementation (NormalizeHost / HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the exact moment-write ordering. The host copies remain the runtime for the DLL and pure-MQL5 tiers and the reference the kernels must match. Because this box has no OpenCL platform, the safety story is layered: - shim validation: kernels compiled as C and driven against a fp64 host transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff 0.132 vs tolerance 1.0 - in-situ self-check: each kernel is compared against its host twin ON FIRST USE on the real device (SelfCheckBn*), covering what the shim cannot - arg indices and buffer bindings. Any disagreement resyncs from the good copy, latches all BN kernels off process-wide, and training continues host-side. A transcription bug costs a warning and some speed, never a poisoned .nnw. - sync discipline: BatchOptions is now a CBufferDouble with explicit authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull read-only; restores/loads/resets push; a mid-batch handover drains the device gamma/beta accumulator into the host arrays so no sample is lost. 3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at init (one warning instead of warning + failed Execute). Build tag bumped to win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five binary-changing commits. Both build variants compile 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if(useDerivedGeometry)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
{
fix: live trades now use the geometry the gate certifies; perf: BN kernels Three changes, one theme: the trade placed, the trade graded, and the trade computed are now the same trade. 1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier pair reached the LABELS only - OpenParams still placed orders at the enum geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before 3.33*ATR above break-even" about trades the EA never placed. Published via g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick contract as the confidence globals, because OpenParams runs on the root signal which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at era 0, and the .cfg adoption a deployed model takes. Overrides both legs and both Intelligent modes - the certificate is exact or it is nothing. TP is ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot reshape the certified target. 2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl - forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a line-for-line transcription of the host implementation (NormalizeHost / HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the exact moment-write ordering. The host copies remain the runtime for the DLL and pure-MQL5 tiers and the reference the kernels must match. Because this box has no OpenCL platform, the safety story is layered: - shim validation: kernels compiled as C and driven against a fp64 host transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff 0.132 vs tolerance 1.0 - in-situ self-check: each kernel is compared against its host twin ON FIRST USE on the real device (SelfCheckBn*), covering what the shim cannot - arg indices and buffer bindings. Any disagreement resyncs from the good copy, latches all BN kernels off process-wide, and training continues host-side. A transcription bug costs a warning and some speed, never a poisoned .nnw. - sync discipline: BatchOptions is now a CBufferDouble with explicit authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull read-only; restores/loads/resets push; a mid-batch handover drains the device gamma/beta accumulator into the host arrays so no sample is lost. 3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at init (one warning instead of warning + failed Execute). Build tag bumped to win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five binary-changing commits. Both build variants compile 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
//--- ATR-anchored like the label, NOT risk-relative: the label measures "reach tp before sl" as
//--- two independent ATR distances from the entry, so the live target must be the same distance -
//--- tying it to the (possibly floor-widened) realised risk would silently reshape the certified
//--- geometry on exactly the trades whose stop got adjusted.
tp = isLong ? m_symbol.NormalizePrice(price + g_DerivedTpAtrMult * atr)
: m_symbol.NormalizePrice(price - g_DerivedTpAtrMult * atr);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
}
else
fix: live trades now use the geometry the gate certifies; perf: BN kernels Three changes, one theme: the trade placed, the trade graded, and the trade computed are now the same trade. 1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier pair reached the LABELS only - OpenParams still placed orders at the enum geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before 3.33*ATR above break-even" about trades the EA never placed. Published via g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick contract as the confidence globals, because OpenParams runs on the root signal which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at era 0, and the .cfg adoption a deployed model takes. Overrides both legs and both Intelligent modes - the certificate is exact or it is nothing. TP is ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot reshape the certified target. 2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl - forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a line-for-line transcription of the host implementation (NormalizeHost / HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the exact moment-write ordering. The host copies remain the runtime for the DLL and pure-MQL5 tiers and the reference the kernels must match. Because this box has no OpenCL platform, the safety story is layered: - shim validation: kernels compiled as C and driven against a fp64 host transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff 0.132 vs tolerance 1.0 - in-situ self-check: each kernel is compared against its host twin ON FIRST USE on the real device (SelfCheckBn*), covering what the shim cannot - arg indices and buffer bindings. Any disagreement resyncs from the good copy, latches all BN kernels off process-wide, and training continues host-side. A transcription bug costs a warning and some speed, never a poisoned .nnw. - sync discipline: BatchOptions is now a CBufferDouble with explicit authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull read-only; restores/loads/resets push; a mid-batch handover drains the device gamma/beta accumulator into the host arrays so no sample is lost. 3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at init (one warning instead of warning + failed Execute). Build tag bumped to win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five binary-changing commits. Both build variants compile 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if(m_tp_mode == TP_INTELLIGENT_MODE)
{
double targetRR = TP_INTELLIGENT_BASE_RR * (1.0 + AI_TP_WIDEN_FACTOR * confidence);
tp = isLong ? m_symbol.NormalizePrice(price + targetRR * risk)
: m_symbol.NormalizePrice(price - targetRR * risk);
}
else
{
double tpMultiplier = (double)m_tp_mode;
tp = isLong ? m_symbol.NormalizePrice(price + tpMultiplier * atr)
: m_symbol.NormalizePrice(price - tpMultiplier * atr);
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
//--- --- 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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
// A widened stop changes this trade's real risk, so recompute it before the reward:risk filter
// below - otherwise the RR the trade is accepted on is not the RR it is actually taken at.
risk = fabs(price - sl);
// 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;
}
//--- REWARD:RISK IS MEASURED AND PUBLISHED, NOT ENFORCED (2026-08-09). The minimum-ratio rejection
//--- that stood here is gone with the Min_Risk_Reward_Ratio input - see Variables\Inputs.mqh.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
double reward = fabs(tp - price);
// Still computed and still bridged to Money\MoneyIntelligent.mqh's Kelly-criterion sizing - the
// ratio remains a genuine INPUT to how big the position should be, which is the use that was
// always sound. Only the veto is gone.
g_TradeRewardRiskRatio = (risk > 0.0) ? reward / risk : 0.0;
// 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)
{
//--- 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;
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
refactor(kiss): drop the AI sub-vote early-exit route; certified == traded First of the AI vote layers to go. CheckClosePosition had two exit routes: the stock blended vote, and an AI-only one reading the AI members' sub-vote undiluted. The second existed because an AI reversal averaged in with the classic filters could be diluted below the threshold before it could close a position. It is gone, and with it m_lastAiVote and the aiResult/aiWeightSum pair Direction() carried to feed it. This CLOSES the certified-vs-traded gap rather than widening it. The deploy gate certifies a win rate measured on hold-to-resolution outcomes, and CheckClosePosition already gated the blended route off whenever an AI model's derived geometry was on the order - so the AI route was the only vote exit an AI-certified trade could take, and the exit replay existed to reproduce it. With it removed, an AI-certified position holds to its barrier by construction instead of by reconstruction, so Warrior_EA.mq5 now pushes ExitPolicy(0.0, true) unconditionally. Previously it forwarded Min_Vote_Close and relied on Disabled arriving as 1.01 to switch the simulated exit off by arithmetic - correct at the shipped default, and one input change away from the simulation and the live path describing different games. Min_Vote_Close keeps its meaning for the classic route and is now documented as inert wherever an AI certificate governs, rather than appearing to drive an exit it can no longer reach. Comment debt cleared while here: a tombstone block for m_ai_exit_threshold (a member deleted 2026-08-18) still sat in the header, and four sites still named LiveSignedConfidence's "two consumers" - it had one, the intelligent trailing stop, since that same date. NOT touched, and deliberately: NMS declustering is NOT a quality layer. It gates the live signal at Inference.mqh:226 (NmsLiveAccept), and the undeclustered population is ~8x what the EA trades. Removing it would multiply live position count, not simplify a scoring path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:27:28 -04:00
//--- ONE EXIT AUTHORITY, and under an AI certificate it is the BARRIER, not a vote.
fix(consistency): one geometry authority, one exit authority, and the MI screen finally gets a veto Consistency pass before a fresh deployment. Three places where two systems were choosing the same thing and one of them silently lost. 1. THE GEOMETRY SCAN'S DECISION WAS INERT - measured, not suspected. USDJPY, 2026-08-17: 14:24:12.844 adopting barrier geometry 2:8 ... Relabelling and training on it. 14:24:12.979 triple-barrier labels - stop 1.61*ATR, target 3.21*ATR ... this is what trains It adopted 2:8 and trained on 1.61:3.21. ReportBarrierGeometryScan wrote only m_sl_mode/m_tp_mode, and BarrierMultiples ranks the DERIVED pair ABOVE those ints - so on any model carrying a derived pair (every model with a .cfg, including a fresh one whose weights are gone but whose sidecar survived) the adoption changed nothing. Worse, had it changed something it would have been undone immediately: the adoption sets m_labelCachePrebuilt = false, and that prebuild re-runs DeriveBarrierGeometry at era 0, which overwrites m_derivedSl/TpMult from the excursion quantiles. ONE AUTHORITY: the derived pair, because it is what the labels read, what the deploy gate certifies, what g_Derived*AtrMult places on the live order, and what the .cfg pins across restarts. The scan now writes THAT (floored by MIN_SL_ATR_MULTIPLIER, the same floor DeriveBarrierGeometry applies so the live stop can never be wider than the labelled one), republishes to the bridge immediately rather than at the next era end, and forces the sidecar to be rewritten. m_geometryAdopted latches it so the derive pass the adoption itself triggers cannot overwrite it. The scan outranks the derive for an evidential reason, not an architectural one: its winner cleared a permutation test against the null of the MAXIMUM over every eligible pairing, and it scores the incumbent derived pair as a peer in that same field. The derive is a descriptive quantile read with no significance test attached. BEHAVIOURAL CHANGE, and the reason to flag it before a fresh test: barrier geometry will now actually move when the scan says so. Until today it never did. 2. ONE EXIT AUTHORITY, tied to whose certificate the trade was placed under. CheckClosePosition had two routes. The AI early-exit reads the AI vote undiluted and is exactly what the new exit replay reproduces. The blended route thresholds m_direction, the average over EVERY filter including classic ones whose live votes pass 3 never computes - so it can close a position the certificate never modelled, and no replay can ever check it. When g_DerivedSlAtrMult > 0 the AI's measured geometry is on the order, which means the deploy gate's certificate is the reason the trade exists. In that state the AI now governs the exit and the blended route is suppressed. Classic-only configurations are untouched: there the blended route is the only exit opinion and stays exactly as it was. Nothing moves at the shipped defaults either way (Min_Vote_Close = Disabled). 3. EDGEFINDER, SECOND HALF: THE MEASUREMENT NOW STEERS. The MI suite has always printed its verdicts and then trained the direction target regardless of what they said. That gap IS the difference between this and the EdgeFinder discipline: measure what the market offers, THEN aim. m_dirEvidence is set when EITHER the feature/label mutual information OR the normalised excursion asymmetry clears its block-permuted null - an OR, because the two look for the same thing by different routes and requiring both would reject on the weaker of two independent measurements. Normalised asymmetry specifically, never the raw one, which is the volatility confound. Deploy - solo AND ensemble - now requires it. A run without it still trains, and keeps its checkpoint: the research value is real and the measurement can be wrong. It simply may not go live. Reported separately from the statistical gate because the remedy is different: a failed selection test says train differently, this says look somewhere else. Excursion SIZE keeps clearing where direction does not, and that is a risk-control head rather than an entry signal. For the ensemble the check is per-chart by construction - the MI suite runs once and shares its outcome across members - which is the honest treatment: four models finding nothing between them is not four chances at an edge, it is four fits to the same absent information. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:43:20 -04:00
bool aiCertificateGoverns = (g_DerivedSlAtrMult > 0.0 && g_DerivedTpAtrMult > 0.0);
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.
if(!aiCertificateGoverns && directionMultiplier * m_direction >= m_threshold_close)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
//--- 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;
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
{
//--- there's a signal
result = true;
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//--- META-LABELING GATE (2026-08-19, user design: the meta head integrated into the voting
//--- decision pipeline). Entries only; exits, SL/TP and the scheduled close-all never consult
//--- it (closing risk must never be blocked).
CMetaGate *metaGate = MetaGate();
if(CheckPointer(metaGate) != POINTER_INVALID)
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
{
double mgP = -1.0, mgBe = -1.0;
if(CMetaGate::Blocks(metaGate.Evaluate(isLong, m_direction, mgP, mgBe, 1)))
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
{
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-meta-veto",
StringFormat("%s: open %s rejected by the meta gate - P(win) %.1f%% below the"
" cost-adjusted break-even %.1f%% (vote %.1f).",
__FUNCTION__, isLong ? "long" : "short",
100.0 * mgP, mgBe, m_direction));
return false;
}
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
//--- 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
//--- 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());
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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(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
// Check if the trading strategy allows opening long positions (INTELLIGENT resolves to the
// measured drift verdict - see WarriorEffectiveDirection)
if(WarriorDirectionAllows(true))
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-long-direction-block",
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
StringFormat("%s: open long rejected - %s blocks long entries.", __FUNCTION__,
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
(tradingdirection == DIRECTION_INTELLIGENT) ? "the measured drift verdict (Intelligent)" : "strategy direction"));
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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(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
// Check if the trading strategy allows opening short positions (INTELLIGENT resolves to the
// measured drift verdict - see WarriorEffectiveDirection)
if(WarriorDirectionAllows(false))
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-short-direction-block",
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
StringFormat("%s: open short rejected - %s blocks short entries.", __FUNCTION__,
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
(tradingdirection == DIRECTION_INTELLIGENT) ? "the measured drift verdict (Intelligent)" : "strategy direction"));
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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) |
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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";
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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) |
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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";
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
return ret;
}
//+------------------------------------------------------------------+
//| 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);
}
//+------------------------------------------------------------------+
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
//| Detecting the "weighted" direction |
//+------------------------------------------------------------------+
double CExpertSignalCustom::Direction(void)
{
//--- 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
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
//--- Open a fresh intra-second averaging window whenever the second changes. This block may ONLY
//--- 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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
int total = m_filters.Total();
PrintVerbose("Starting direction calculation with total filters: " + IntegerToString(total));
//--- 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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
JournalFilterPatterns(filter, brokerTime);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
double direction = filter.Direction();
//--- 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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
2026-08-23 09:15:06 -04:00
vote.Add(signedDir, filter.VoteCapableWeight());
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
}
}
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".
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.");
}
//--- 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
{
//--- 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
}
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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};
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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");
}
//--- 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))
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
if(!dbm.FetchRecordCount(oppositeTableName, oppCount))
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
if(curCount >= m_maxTableRows)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
DeleteOldestEntry(currentTableName);
2026-08-12 18:53:04 -04:00
if(oppCount >= m_maxTableRows)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
DeleteOldestEntry(oppositeTableName);
//--- 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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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));
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
//+------------------------------------------------------------------+
//| 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);
if(MathIsValidNumber(mag) && mag > m_votePeak)
m_votePeak = mag;
//--- The peak SHOWN is the larger of the live peak and the overlay census's strongest vote.
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
double peak = MathMax(m_votePeak, m_overlayBestNet);
//--- A PROSPECTIVE vote can never be a trade, however high it reads - the models are not
//--- deployed. Saying "-> TRADE" on a number that cannot place an order would be the exact
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
//--- overstatement this readout exists to prevent.
bool fires = (mag >= m_threshold_open) && (voters > 0) && !prospective &&
(vote == 0.0 || WarriorDirectionAllows(vote > 0.0));
//--- THE HEADLINE WORD IS THE DECISION, NOT THE LEAN (user request 2026-08-19).
bool clears = (voters > 0) && (vote != 0.0) && (mag >= m_threshold_open);
string dir = (voters <= 0 && neutrals <= 0) ? "--"
: (clears ? (vote > 0.0 ? "BUY" : "SELL") : "NEUTRAL");
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
//--- Consolas so the columns line up as the numbers change width - a readout that jitters is one
//--- you have to re-read every time instead of glancing at.
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
string verdict = prospective
? "-> 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);
string txt = StringFormat("VOTE %s %+5.1f%% peak %5.1f%% need %.0f%% %s %s",
dir, vote, peak, m_threshold_open, 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
string nm = VOTE_HUD_PREFIX;
if(ObjectFind(0, nm) < 0)
{
//--- ObjectFind is affordable HERE, unlike in the arrow paths: this is ONE object refreshed once
//--- per bar, not thousands created in a sweep. The O(n^2) rule that bans the pre-check there is
//--- about per-object cost in a loop, and applying it blindly here would just leak properties.
ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, 18);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, nm, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
ObjectSetString(0, nm, OBJPROP_TEXT, txt);
//--- Colour carries the verdict so the line can be read without parsing it: green/red only when
//--- the vote would actually place an order, grey otherwise. Prospective reads dimmer than "no
//--- trade" so the two are never confused at a glance.
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
ObjectSetInteger(0, nm, OBJPROP_COLOR,
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
fires ? (vote > 0.0 ? clrLime : clrRed)
: (prospective ? clrDimGray : clrSilver));
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
}
//+------------------------------------------------------------------+
//| 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
feat(hud): per-member neuron lines + a vote label that moves as the nets learn Both 2026-08-19 reports were the same staleness: every source behind the label was an ERA artifact (live cache refills at pass-3 completion, the snapshot copies once per era, dPrevSignal is the frozen purge-band edge bar) - so the readout stepped at era cadence at best, stayed glued to one direction, and lagged the era counter. DisplayInference(): throttled (4s, 1s across an era boundary), SIDE-EFFECT-FREE forward of the current decision bar (window ending on bar 1, same question the live path asks) through the LEARNER net. Batch-norm running stats are bracketed frozen/RESTORED via the new CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore, not unfreeze, because a display tick can land between pass-3 chunks whose whole scan holds them frozen. Writes nothing a trading or training path reads (dPrevSignal, NMS state, tallies, watermarks all untouched; RefreshLatestSignal is not reusable here precisely because it writes all of them). LSTM safe by construction: h/c zeroed per forward. ProspectiveVote() reads the fresh forward as its FIRST source; the era-artifact chain becomes the fallback (meta head, warm-up, window holes). DisplayHudLine(): the reference library's training label, per ensemble member - name, output activations (softmax probs or raw scalar), the decision, its weighted vote (the exact consensus numerator term), era, recent average error, "(trn)" while not vote-capable. Rendered under the vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines are telemetry, not tradable readings), coloured by the member's own direction in muted tones - the vote line's strict green-only-when-it-would-trade rule is untouched. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00
//--- PER-MEMBER NEURON LINES, rendered BEFORE the live-vote defer below: the defer protects the
//--- aggregate VOTE line (a tradable reading must not be repainted with an untradable one), but
//--- the member lines are not tradable readings in the first place - they are the training
//--- telemetry, and freezing them for a whole bar because a live vote exists would re-create the
//--- exact only-moves-once-per-era staleness they were built to end.
int hudLine = 0;
for(int hi = 0; hi < total; hi++)
{
CExpertSignalCustom *hf = m_filters.At(hi);
if(hf == NULL || (m_ignore & (((long)1) << hi)) != 0)
continue;
string hudTxt = hf.DisplayHudLine();
if(hudTxt == "")
continue; // classic ladders and veto filters draw no neuron line
//--- Colour = the member's own current direction (muted tones - these are opinions, not
//--- orders; the vote line's strict green-only-when-it-would-trade rule stays untouched).
double hv = 0.0, hw = 0.0;
hf.ProspectiveVote(hv, hw); // cached: the throttled forward already ran inside DisplayHudLine
if((m_invert & (((long)1) << hi)) != 0)
hv = -hv;
string nm = VOTE_HUD_PREFIX + StringFormat("_m%02d", hudLine);
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, 34 + 14 * hudLine);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 8);
ObjectSetString(0, nm, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
ObjectSetString(0, nm, OBJPROP_TEXT, hudTxt);
ObjectSetInteger(0, nm, OBJPROP_COLOR,
(hv > 0.0) ? clrMediumSeaGreen : (hv < 0.0 ? clrIndianRed : clrSilver));
hudLine++;
}
for(int hd = hudLine; hd < m_hudMemberLines; hd++)
ObjectDelete(0, VOTE_HUD_PREFIX + StringFormat("_m%02d", hd));
m_hudMemberLines = hudLine;
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(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);
//--- 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;
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;
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
//--- 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;
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;
}
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". |
//+------------------------------------------------------------------+
double CExpertSignalCustom::HistoricalNetVote(const int idx, double &capableOut)
{
2026-08-23 09:15:06 -04:00
SVoteAccumulator vote;
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;
if((m_invert & mask) != 0)
contribution = -contribution;
2026-08-23 09:15:06 -04:00
vote.Add(contribution, filter.VoteCapableWeight());
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-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
}
//+------------------------------------------------------------------+
//| 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
{
//--- 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;
double net = HistoricalNetVote(idx, den);
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.
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
//--- 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;
//--- The direction policy (LONG_ONLY/SHORT_ONLY, or the Intelligent drift verdict) gates the
//--- reconstruction exactly as it gates CheckOpenLong/Short live: a blocked side falls into
//--- 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): 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
//--- 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++;
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;
//--- 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;
Print(StringFormat("Filtered view: swept %d bar(s), %d had a voter (%d buy / %d sell), drew %d"
" arrow(s). Strongest vote %.1f%% against a %.1f%% threshold.%s",
m_overlaySweptBars, m_overlayVotedBars, m_overlayVotedBuy, m_overlayVotedSell,
m_overlayDrawn, m_overlayBestNet, m_threshold_open,
(m_overlayVotedBars == 0
? " No member has a completed era yet (snapshots fill at each member's first"
" pass-3 completion) and every classic signal is disabled."
: (m_overlayDrawn == 0
? " The models voted but never strongly enough; this is the vote"
" failing the bar, not the drawing failing."
: ""))));
}
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;
}
//+------------------------------------------------------------------+
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. |
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
}
//+------------------------------------------------------------------+
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)};
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
//--- 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;
//--- 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;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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).
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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))
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print(__FUNCTION__ + " Failed to count outcomes in " + tableNameBuy);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
continue;
}
2026-08-12 18:53:04 -04:00
if(!dbm.FetchWinLossCounts(tableNameSell, nowKey, winsSell, lossesSell))
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print(__FUNCTION__ + " Failed to count outcomes in " + tableNameSell);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
//--- ...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())
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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++;
}
}
// Track the overall DB win-rate confidence across all filters, so it can be
// combined with (or used instead of) AI confidence via Confidence_Source.
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) |
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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)
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
int totalTrades = wins + losses;
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
if(totalTrades < MIN_TRADES_FOR_WIN_RATE)
return NO_DATA_WIN_RATE;
//--- 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));
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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();
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
//--- check pointer
if(filter == NULL)
continue;
//--- 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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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);
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
}
//+------------------------------------------------------------------+