fix: the recall gate was unsatisfiable and the LR decay was a spiral
Both made the run structurally unable to succeed, independently of any
signal in the data. Found by reading the 13:01 log.
RECALL GATE. m_objectiveMet required Buy, Sell AND Neutral OOS recall
each >= 40%. First-touch resolution (ce52654) collapsed Neutral from
the ~94% majority it was under exact-pivot labels to a same-bar-tie
residue - 250 of 38,261 bars, 0.65% - so the floor was asking the model
to identify 40% of coin-flip ties before it could converge. Measured:
CONV, LSTM and HYBRID all logged "Neutral:0% (need >=40% each)" on
every era. No model could ever satisfy it; every run was destined for
the plateau ladder or the era cap.
Only the DIRECTIONAL floors are load-bearing for the anti-collapse job
the gate exists to do: an all-Neutral model shows Buy and Sell recall
at 0% and is blocked by them. Neutral's own floor guarded the mirror
bias (over-calling Buy/Sell at Neutral's expense), which was real at
94% prevalence and is not at 0.65% - there, almost never calling
Neutral is correct rather than biased.
Prevalence-guarded rather than hardcoded off, so it returns by itself
if a future label rule makes Neutral substantial again. Deliberately
NOT extended to Buy/Sell: exempting a thin directional class reopens
the era-44-46 hole, which directionalRecallMeasured only half-covers -
it checks those classes were MEASURED, not that they passed.
ETA DECAY. A regressing era restored the checkpoint, reset the
optimizer and cut eta - all on the FIRST regression. The next era then
started from an identical state with a smaller step, regressed again,
and got the same treatment. The loop is self-sustaining and cannot
discover anything, because rolling the weights back is exactly what
removes the exploration that would end it.
Measured on PAI: eras 2-11 every one a regression against era 1, eta
0.000594 -> 0.000024, dW/W 0.000%/0.000% from era 2 onward. Ten eras,
~45s each, reproducing era 1 exactly and unable to do anything else.
Now requires ETA_DECAY_PATIENCE_ERAS consecutive regressions - the
standard ReduceLROnPlateau formulation. A single bad era is noise, and
an improving era clears the counter so alternating runs never
accumulate into a decay.
Build tag -> gate-patience-v3. It had not moved in six commits, which
is why the running binary could not be identified from its own log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:28:58 -04:00
//+------------------------------------------------------------------+
2026-07-14 22:36:27 -04:00
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
2026-08-23 16:40:27 -04:00
//--- THIS IS THE PRIVATE / PROP-FIRM BUILD, AND THE ONLY ONE. The MQL5 Market variant is gone
//--- (2026-08-23): Market rule IV forbids DLL calls, and the DLL compute tier plus the WebRequest
//--- alt-data fetch are what make this bot work at all. If it is ever sold it goes through its own
//--- channel with the DLLs intact, so there is nothing left for a no-DLL build to be for. Custom
//--- indicators load by bare name from <MQL5>\Indicators\; nothing is embedded as a #resource.
2026-08-22 00:25:52 -04:00
//--- Inputs FIRST so the EA's own grouped inputs lead the Inputs tab. Safe because Inputs.mqh
//--- depends only on Enumerations\InputEnums.mqh (which carries a guarded ENUM_OPTIMIZATION copy) -
//--- no AI header needed.
2026-07-22 17:17:23 -04:00
# include "Variables\Inputs.mqh"
2026-08-16 15:12:54 -04:00
//--- chart-level tuned indicator periods: read at OnInit before the DB fingerprint and the classic
//--- signal configuration, written by a gated auto-tune install - see the file's header contract
# include "Variables\TunedPeriods.mqh"
2026-07-14 22:36:27 -04:00
//--- database classes
# include "Database\DatabaseManager.mqh"
2026-07-22 22:51:04 -04:00
# include "Database\TradeJournalManager.mqh"
2026-07-14 22:36:27 -04:00
//--- available custom classes
# include "Expert\ExpertCustom.mqh"
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- Persistence for the COMBINED-VOTE arrow layer - the one the chart actually shows while
//--- DrawUnfilteredSignals is off. Must come after ExpertCustom.mqh: it builds on SIG_VOTE_PREFIX
//--- and WarriorPlotSignalLevel, which ExpertSignalCustom.mqh defines.
# include "Expert\Chart\VoteArrows.mqh"
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and
the lattice structure that shape of generator has. Two places here
actually lean on randomness and both were hurt by it:
WEIGHT INIT. Six He/LeCun-uniform sites drew
((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of
~250k weights had only 32768 possible values and thousands of
connections started byte-identical. Breaking that symmetry is the whole
job of random init.
SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand()
draws to reach 30 bits, and its own comment documented the residual
modulo bias it still carried. HQRndUniformI() is rejection-sampled and
exactly uniform, so the splice and the bias note both go.
CHighQualityRand is L'Ecuyer's combined multiplicative congruential
generator - two differenced streams, 31-bit output, period ~2.3e18 -
and it ships with the terminal.
AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount())
calls sit immediately before "build a fresh topology", once per model.
GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every
member inside one OnInit, so members could be handed the SAME seed and
draw the SAME weights wherever their shapes coincide - and members that
start identical are not an ensemble. WarriorRandSeed() takes a salt (the
model id) plus a never-reset call counter, so a collision is impossible
rather than merely unlikely, while the tick keeps the run itself
genuinely unrepeatable the way those call sites asked for.
Seeds are masked positive rather than trusted: HQRndSeed computes
s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the
generator in a state its own assertions reject. GetTickCount() is a uint
and goes negative as an int after ~24 days of uptime - a fault that
would surface as "training is broken" on a long-running terminal and
nowhere else.
The indicator tuner's 52 draws move across too: its random search is
where sample quality earns its keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
# include "System\Random.mqh"
2026-07-14 22:36:27 -04:00
# include "System\PrintVerbose.mqh"
2026-07-17 21:28:59 -04:00
# include "System\StatusLabel.mqh"
2026-08-16 13:59:03 -04:00
# include "System\AltDataFetch.mqh"
2026-07-14 22:36:27 -04:00
//--- available signals
# include "Signals\Signals.mqh"
//--- available trailing
# include "Trailing\Trailing.mqh"
//--- available money management
# include "Money\Money.mqh"
//--- Variables
# include "Variables\Variables.mqh"
//--- Control panel GUI (standard MQL5 Controls library)
# include "Panel\ControlPanel.mqh"
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//--- One-time "which instrument is this?" dialog for symbols the alt-data catalog does not know
# include "Panel\AltDataMapDialog.mqh"
2026-07-14 22:36:27 -04:00
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| The CustomIndicators\*.mq5 files (ADCumulativeDelta, |
//| ADShorteningOfThrust, ADWyckoffEventStream, |
//| ADWyckoffFailedStructure, ADWyckoffSignificantBarInversion) are |
//| loaded via CiCustom/IND_CUSTOM (see ExpertSignalAIBase.mqh). |
2026-07-14 22:36:27 -04:00
//+------------------------------------------------------------------+
//
CExpertCustom Expert ;
CDatabaseManager dbm ( ) ;
2026-07-22 22:51:04 -04:00
CTradeJournalManager journal ;
2026-07-14 22:36:27 -04:00
//+------------------------------------------------------------------+
//| Pointers to whichever AI signal instances this run actually |
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
//| created (any enabled Use_* subset, plus the meta head), so the |
2026-07-14 22:36:27 -04:00
//| panel can drive training/weight actions on exactly the signal(s) |
//| in play this run and never touch another config's files. |
//+------------------------------------------------------------------+
2026-08-22 00:25:52 -04:00
//--- Must be >= the number of AI signal instances one run can create at once. At 3 the CONVLSTM
//--- member was once silently dropped on the floor (609be10); the array holds borrowed pointers, so
//--- headroom is free - but a cap that silently discards a model is a trapdoor, hence the loud
//--- refusal below.
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped
AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers
them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned
silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[].
Reported as "convlstm is not listening to the control panel buttons", which is
the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by
looping g_aiSignals[], so the dropped member also lost:
- every control panel button (pause/resume, stop/start, retrain, deploy,
save, load, reset weights)
- PollTraining() in OnTimer - no wall-clock training progress, so it only
advanced on ticks
- AutosaveWeightsIfDue() -> SaveWeightsNow()
- AltDataReload() on both the mapping-dialog and hourly-upkeep paths
- StartChartSignalRescan()/RescanPending() - the Show Signals sequence
- the All*/Any* aggregates (deployed/paused/stopped/complete), which were
therefore computed over 3 of 4 members and could report the ensemble
finished while CONVLSTM was still training
- OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() -
so its arrows were stranded on the chart and its training run was never
flushed on shutdown
It stayed hidden because the model still trains and still votes: it lives in
the signal's own filter array, and it registers itself with the status panel
(ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it
appeared on the panel, drew arrows and moved the vote while being unreachable
from every action and unsaveable on exit.
MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding
META to a preset cannot reintroduce this - the array holds borrowed pointers,
so unused slots cost nothing).
RegisterAISignal now PRINTS on overflow instead of returning silently. A cap
that discards a model without saying so is a trapdoor, not a guard.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
# define MAX_AI_SIGNALS 5
2026-07-27 15:52:39 -04:00
//--- Printed at OnInit so tester logs prove which binary is actually running.
2026-08-27 16:16:40 -04:00
# define WARRIOR_BUILD_TAG " cooldown-recon2 "
2026-07-14 22:36:27 -04:00
CExpertSignalAIBase * g_aiSignals [ MAX_AI_SIGNALS ] ;
int g_aiSignalCount = 0 ;
void RegisterAISignal ( CExpertSignalAIBase * sig )
{
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped
AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers
them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned
silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[].
Reported as "convlstm is not listening to the control panel buttons", which is
the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by
looping g_aiSignals[], so the dropped member also lost:
- every control panel button (pause/resume, stop/start, retrain, deploy,
save, load, reset weights)
- PollTraining() in OnTimer - no wall-clock training progress, so it only
advanced on ticks
- AutosaveWeightsIfDue() -> SaveWeightsNow()
- AltDataReload() on both the mapping-dialog and hourly-upkeep paths
- StartChartSignalRescan()/RescanPending() - the Show Signals sequence
- the All*/Any* aggregates (deployed/paused/stopped/complete), which were
therefore computed over 3 of 4 members and could report the ensemble
finished while CONVLSTM was still training
- OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() -
so its arrows were stranded on the chart and its training run was never
flushed on shutdown
It stayed hidden because the model still trains and still votes: it lives in
the signal's own filter array, and it registers itself with the status panel
(ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it
appeared on the panel, drew arrows and moved the vote while being unreachable
from every action and unsaveable on exit.
MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding
META to a preset cannot reintroduce this - the array holds borrowed pointers,
so unused slots cost nothing).
RegisterAISignal now PRINTS on overflow instead of returning silently. A cap
that discards a model without saying so is a trapdoor, not a guard.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
if ( sig = = NULL )
2026-07-14 22:36:27 -04:00
return ;
2026-08-22 00:25:52 -04:00
//--- LOUD on overflow. A cap that silently discards a model is not a guard, it is a trapdoor.
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped
AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers
them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned
silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[].
Reported as "convlstm is not listening to the control panel buttons", which is
the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by
looping g_aiSignals[], so the dropped member also lost:
- every control panel button (pause/resume, stop/start, retrain, deploy,
save, load, reset weights)
- PollTraining() in OnTimer - no wall-clock training progress, so it only
advanced on ticks
- AutosaveWeightsIfDue() -> SaveWeightsNow()
- AltDataReload() on both the mapping-dialog and hourly-upkeep paths
- StartChartSignalRescan()/RescanPending() - the Show Signals sequence
- the All*/Any* aggregates (deployed/paused/stopped/complete), which were
therefore computed over 3 of 4 members and could report the ensemble
finished while CONVLSTM was still training
- OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() -
so its arrows were stranded on the chart and its training run was never
flushed on shutdown
It stayed hidden because the model still trains and still votes: it lives in
the signal's own filter array, and it registers itself with the status panel
(ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it
appeared on the panel, drew arrows and moved the vote while being unreachable
from every action and unsaveable on exit.
MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding
META to a preset cannot reintroduce this - the array holds borrowed pointers,
so unused slots cost nothing).
RegisterAISignal now PRINTS on overflow instead of returning silently. A cap
that discards a model without saying so is a trapdoor, not a guard.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
if ( g_aiSignalCount > = MAX_AI_SIGNALS )
{
Print ( __FUNCTION__ + " : CANNOT REGISTER a further AI signal - MAX_AI_SIGNALS is " +
IntegerToString ( MAX_AI_SIGNALS ) + " and this run already created that many. The extra "
" model would still train and still vote, but the control panel could not reach it and its "
" weights would never be autosaved or flushed on shutdown. Raise MAX_AI_SIGNALS to at least "
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
" the number of instances the enabled Use_* inputs create and recompile. " ) ;
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped
AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers
them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned
silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[].
Reported as "convlstm is not listening to the control panel buttons", which is
the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by
looping g_aiSignals[], so the dropped member also lost:
- every control panel button (pause/resume, stop/start, retrain, deploy,
save, load, reset weights)
- PollTraining() in OnTimer - no wall-clock training progress, so it only
advanced on ticks
- AutosaveWeightsIfDue() -> SaveWeightsNow()
- AltDataReload() on both the mapping-dialog and hourly-upkeep paths
- StartChartSignalRescan()/RescanPending() - the Show Signals sequence
- the All*/Any* aggregates (deployed/paused/stopped/complete), which were
therefore computed over 3 of 4 members and could report the ensemble
finished while CONVLSTM was still training
- OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() -
so its arrows were stranded on the chart and its training run was never
flushed on shutdown
It stayed hidden because the model still trains and still votes: it lives in
the signal's own filter array, and it registers itself with the status panel
(ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it
appeared on the panel, drew arrows and moved the vote while being unreachable
from every action and unsaveable on exit.
MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding
META to a preset cannot reintroduce this - the array holds borrowed pointers,
so unused slots cost nothing).
RegisterAISignal now PRINTS on overflow instead of returning silently. A cap
that discards a model without saying so is a trapdoor, not a guard.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
return ;
}
2026-07-14 22:36:27 -04:00
g_aiSignals [ g_aiSignalCount + + ] = sig ;
}
//+------------------------------------------------------------------+
//| Control panel: a CAppDialog-based GUI (see Panel\ControlPanel.mqh) |
//| with show/hide signals, start/pause/stop training, and save/load/ |
//| delete-reset weights buttons for the currently-active AI |
//| signal(s) only. The dialog's own caption bar provides the show/ |
//| hide (minimize) control - no separate toggle button needed. |
//+------------------------------------------------------------------+
2026-07-17 21:28:59 -04:00
//--- default spawn position: top-right corner, clear of the status label text block (top-left) so the
2026-07-14 22:36:27 -04:00
//--- two don't overlap on first run - the panel is fully draggable afterwards via its caption bar,
//--- so this is only a starting point, not a constraint.
# define CP_Y0 10
# define CP_RIGHT_MARGIN 80
CControlPanel ExtPanel ;
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//--- Alt-data maintenance + the one-time symbol-mapping dialog. Declared HERE rather than beside
//--- OnTimer because OnDeinit (further up the file) tears the dialog down, and MQL5 resolves global
//--- variables in declaration order.
# define ALTDATA_CHECK_SECONDS 1800
datetime g_lastAltDataRun = 0 ;
CAltDataFetch g_altDataFetch ;
CAltDataMapDialog g_altMapDialog ;
bool g_altMapDialogOpen = false ;
bool g_altMapAsked = false ; // one prompt per attach, even if the user closes it unanswered
2026-07-14 22:36:27 -04:00
bool g_signalsVisible = true ;
2026-07-27 11:13:19 -04:00
# define SIGNAL_VISIBILITY_STATE_SUFFIX " .sigvis "
fix(chart): stale combined-vote arrows survived every wipe, because two files lived outside Warrior_EA\
Operator report: arrows labelled as restored from a previous session on a
fleet training from era 0. Confirmed - all six charts restored 115-431
combined-vote arrows drawn by models that no longer exist.
TWO INDEPENDENT DEFECTS, either of which alone causes it.
1. CVoteArrowStore::Discard() HAD NO CALLER.
The member-scoped .arrows file is cleared by ClearPersistedChartSignals on a
fresh topology. The CHART-scoped .votearrows store has an equivalent
Discard(), written for exactly this, and nothing ever called it. The store
is keyed on the DB config fingerprint, which does not move when a model is
wiped, so it reloaded across any reset - fresh topology, panel weight reset,
or a model-file wipe.
A vote is a claim made by a specific set of members. If any member rebuilt
from scratch this run, the whole stored history is void, so
g_warriorFreshTopologyThisRun is now raised wherever a member discards
weights or builds a fresh topology, and the store Discards instead of Loads.
2. TWO WARRIOR FILES LIVED OUTSIDE Warrior_EA\.
.sigvis and .votearrows were written to the ROOT of Common\Files, outside
the one directory that "wipe the Warrior EA files" has always meant. Two
consecutive wipes this session left them standing untouched, and neither
wipe was as fresh as reported. Both now live under Warrior_EA\ChartState\.
A wipe that does not remove all of a program's state is not a wipe, and
nothing in the log told the operator which files were missed.
NOTE for anyone re-running the wipe: pre-existing WarriorVote_*.votearrows and
Warrior_EA_*.sigvis in the Common\Files ROOT are orphaned by this change and
should be deleted once.
Build tag -> fleet-pool-v3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 21:07:52 -04:00
//--- EVERY WARRIOR FILE LIVES UNDER Warrior_EA\, AND TWO OF THEM DID NOT (fixed 2026-08-26).
//--- The .sigvis and .votearrows sidecars were written to the ROOT of Common\Files, outside the
//--- one directory that "wipe the Warrior EA files" has always meant. Two consecutive wipes
//--- therefore left them standing, and the second fresh start restored 115-431 combined-vote arrows
//--- per chart onto models training from era 0. A wipe that does not remove all of a program's
//--- state is not a wipe, and the operator has no way to know which files were missed.
# define WARRIOR_STATE_DIR " Warrior_EA \\ "
2026-07-27 11:13:19 -04:00
string SignalsVisibilityStateFile ( void )
{
fix(chart): stale combined-vote arrows survived every wipe, because two files lived outside Warrior_EA\
Operator report: arrows labelled as restored from a previous session on a
fleet training from era 0. Confirmed - all six charts restored 115-431
combined-vote arrows drawn by models that no longer exist.
TWO INDEPENDENT DEFECTS, either of which alone causes it.
1. CVoteArrowStore::Discard() HAD NO CALLER.
The member-scoped .arrows file is cleared by ClearPersistedChartSignals on a
fresh topology. The CHART-scoped .votearrows store has an equivalent
Discard(), written for exactly this, and nothing ever called it. The store
is keyed on the DB config fingerprint, which does not move when a model is
wiped, so it reloaded across any reset - fresh topology, panel weight reset,
or a model-file wipe.
A vote is a claim made by a specific set of members. If any member rebuilt
from scratch this run, the whole stored history is void, so
g_warriorFreshTopologyThisRun is now raised wherever a member discards
weights or builds a fresh topology, and the store Discards instead of Loads.
2. TWO WARRIOR FILES LIVED OUTSIDE Warrior_EA\.
.sigvis and .votearrows were written to the ROOT of Common\Files, outside
the one directory that "wipe the Warrior EA files" has always meant. Two
consecutive wipes this session left them standing untouched, and neither
wipe was as fresh as reported. Both now live under Warrior_EA\ChartState\.
A wipe that does not remove all of a program's state is not a wipe, and
nothing in the log told the operator which files were missed.
NOTE for anyone re-running the wipe: pre-existing WarriorVote_*.votearrows and
Warrior_EA_*.sigvis in the Common\Files ROOT are orphaned by this change and
should be deleted once.
Build tag -> fleet-pool-v3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 21:07:52 -04:00
return WARRIOR_STATE_DIR + " ChartState \\ " + eaName + " _ " + Symbol ( ) + " _ " + IntegerToString ( Period ( ) ) + SIGNAL_VISIBILITY_STATE_SUFFIX ;
2026-07-27 11:13:19 -04:00
}
bool LoadSignalsVisibilityState ( void )
{
if ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) )
return false ;
string stateFile = SignalsVisibilityStateFile ( ) ;
if ( ! FileIsExist ( stateFile , FILE_COMMON ) )
return false ;
int handle = FileOpen ( stateFile , FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE ) ;
if ( handle = = INVALID_HANDLE )
return false ;
int storedVisible = FileReadInteger ( handle ) ;
FileClose ( handle ) ;
g_signalsVisible = ( storedVisible ! = 0 ) ;
return true ;
}
bool SaveSignalsVisibilityState ( void )
{
if ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) )
return true ;
string stateFile = SignalsVisibilityStateFile ( ) ;
int handle = FileOpen ( stateFile , FILE_COMMON | FILE_BIN | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE ) ;
if ( handle = = INVALID_HANDLE )
{
Print ( __FUNCTION__ + " : failed to open " + stateFile + " for write, error " + IntegerToString ( GetLastError ( ) ) ) ;
return false ;
}
FileWriteInteger ( handle , g_signalsVisible ? 1 : 0 , INT_VALUE ) ;
FileClose ( handle ) ;
return true ;
}
2026-07-26 12:52:56 -04:00
//--- true while Show Signals has queued a rescan on one or more g_aiSignals and is waiting for all of
//--- them to finish (see ToggleSignalsVisibility/FinalizeSignalsRescanIfDone) - each signal's own rescan
//--- is now chunked across PollTraining's timer slices (CExpertSignalAIBase::AdvanceChartSignalRescan)
//--- instead of blocking the button click, so visibility can only be (re)applied and the "shown" Alert
//--- fired once every instance's RescanPending() has cleared.
bool g_signalsRescanPending = false ;
2026-07-14 22:36:27 -04:00
//--- tracks the last known AlgoTrading permission state (terminal "Algo Trading" toggle AND this
//--- EA's own "Allow Algo Trading" property) so a change is logged exactly once, not spammed every tick
bool g_lastAlgoTradingAllowed = true ;
//--- OnDeinit() is not guaranteed to run on a terminal crash/power loss/forced kill, so weights
2026-08-22 00:25:52 -04:00
//--- would otherwise only be as fresh as the last fully-completed training era.
2026-07-26 10:27:38 -04:00
datetime g_lastAutosaveBarTime = 0 ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
//--- TESTER PASS SELF-PROFILE. Three coarse buckets accumulated per tick (plus the timer's own),
//--- printed once at the pass's OnDeinit - so a slow pass NAMES its own consumer instead of being
//--- guessed at from the outside (the 2026-08-25 "0.1% an hour" report took a day of guessing that
//--- one PrintFormat would have answered). Two clock reads per tick when active, zero when live.
bool g_tpActive = false ;
long g_tpTicks = 0 , g_tpTimers = 0 ;
ulong g_tpPreUs = 0 , g_tpExpertUs = 0 , g_tpJournalUs = 0 , g_tpTimerUs = 0 ;
2026-07-25 16:39:11 -04:00
//--- last observed AllTrainingDeployed() value, so OnTimer() can spot training deploying itself (plateau
//--- ladder / era cap) and resync the panel's button labels exactly once on the transition
bool g_lastDeployedState = false ;
2026-07-14 22:36:27 -04:00
//--- summarizes state across all currently-active AI signals for button labels;
//--- "paused"/"stopped" only report true if EVERY active signal agrees, so a mixed state
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
//--- (e.g. several NNs enabled with one paused and one running) still shows an actionable label
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
//--- Counted over the SIGNAL TREE, not over g_aiSignals[]: the tree is the population a panel command
//--- actually reaches, so a label can no longer describe a different set of models than the button acts
//--- on. SIGTRAIT_TRAINABLE is the denominator - "all paused" means nothing without it.
int TrainableSignalCount ( void ) { return Expert . CountSignalTrait ( SIGTRAIT_TRAINABLE ) ; }
2026-07-14 22:36:27 -04:00
bool AllTrainingPaused ( void )
{
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
int n = TrainableSignalCount ( ) ;
return n > 0 & & Expert . CountSignalTrait ( SIGTRAIT_TRAINING_PAUSED ) = = n ;
2026-07-14 22:36:27 -04:00
}
bool AllTrainingStopped ( void )
{
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
int n = TrainableSignalCount ( ) ;
return n > 0 & & Expert . CountSignalTrait ( SIGTRAIT_TRAINING_STOPPED ) = = n ;
2026-07-14 22:36:27 -04:00
}
2026-08-22 00:25:52 -04:00
//--- "deployed" = every active signal has finalised a model and is running live inference rather
//--- than training.
2026-07-25 16:39:11 -04:00
bool AllTrainingDeployed ( void )
{
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
int n = TrainableSignalCount ( ) ;
return n > 0 & & Expert . CountSignalTrait ( SIGTRAIT_TRAINING_COMPLETE ) = = n ;
2026-07-25 16:39:11 -04:00
}
//--- true only while at least one signal is still trainable AND has never checkpointed an era that
//--- cleared the per-class recall floor - i.e. deploying right now would ship a model that ignores Buy
//--- or Sell. Same bar the plateau ladder's automatic deploy refuses to cross on its own.
bool AnyDeployWouldSkipRecallFloor ( void )
{
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
return Expert . CountSignalTrait ( SIGTRAIT_DEPLOY_SKIPS_RECALL ) > 0 ;
2026-07-25 16:39:11 -04:00
}
2026-07-14 22:36:27 -04:00
void ApplySignalsVisibility ( void )
{
2026-08-22 00:25:52 -04:00
//--- TYPED-BLIND, PREFIX-SCOPED. A mark is a line AND an arrow (see WarriorPlotSignalLevel), so
//--- a type-filtered sweep would toggle half of each one and leave the chart showing arrows for
//--- signals whose levels are hidden.
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
for ( int i = ObjectsTotal ( 0 , -1 , -1 ) - 1 ; i > = 0 ; i - - )
2026-07-14 22:36:27 -04:00
{
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 name = ObjectName ( 0 , i , -1 , -1 ) ;
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
if ( StringFind ( name , SIG_ARROW_PREFIX ) ! = 0 )
continue ;
2026-07-14 22:36:27 -04:00
ObjectSetInteger ( 0 , name , OBJPROP_TIMEFRAMES , g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS ) ;
}
ChartRedraw ( 0 ) ;
}
void ToggleSignalsVisibility ( void )
{
g_signalsVisible = ! g_signalsVisible ;
2026-08-22 00:25:52 -04:00
//--- Hide->Show is also the operator's manual "these arrows look stale" refresh: rescan each
//--- deployed model against recent history BEFORE re-showing, so Show Signals reveals a fresh
//--- set instead of just re-exposing whatever old render the .arrows sidecar happened to hold
//--- (see CExpertSignalAIBase::StartChartSignalRescan/AdvanceChartSignalRescan).
2026-07-26 12:36:56 -04:00
if ( g_signalsVisible )
2026-07-26 12:52:56 -04:00
{
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
bool anyQueued = ( Expert . DispatchSignalCommand ( SIGCMD_RESCAN_SIGNALS ) > 0 ) ;
2026-07-26 12:52:56 -04:00
g_signalsRescanPending = anyQueued ;
if ( anyQueued )
{
//--- Immediate feedback that the click registered - the real "Hide Signals" label only lands
//--- once FinalizeSignalsRescanIfDone() runs RefreshControlPanelLabels() below.
ExtPanel . SetSignalsText ( " Scanning... " ) ;
return ; // ApplySignalsVisibility()/labels/Alert deferred to FinalizeSignalsRescanIfDone()
}
}
2026-07-27 11:13:19 -04:00
ApplySignalsVisibility ( ) ;
SaveSignalsVisibilityState ( ) ;
2026-07-26 12:52:56 -04:00
}
//--- Called every OnTimer tick while g_signalsRescanPending: applies visibility and fires the "shown"
//--- Alert only once every queued rescan (see ToggleSignalsVisibility) has finished, since the arrows
//--- being toggled visible don't exist yet until each instance's AdvanceChartSignalRescan completes.
void FinalizeSignalsRescanIfDone ( void )
{
if ( ! g_signalsRescanPending )
return ;
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 ( Expert . CountSignalTrait ( SIGTRAIT_RESCAN_PENDING ) > 0 )
return ; // at least one instance still scanning - check again next tick
2026-07-26 12:52:56 -04:00
g_signalsRescanPending = false ;
2026-07-14 22:36:27 -04:00
ApplySignalsVisibility ( ) ;
2026-07-26 12:52:56 -04:00
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: signal arrows shown " ) ;
2026-07-14 22:36:27 -04:00
}
//--- keeps every button's label in sync with live training/signal-visibility state; safe/cheap to
//--- call after every panel action
void RefreshControlPanelLabels ( void )
{
ExtPanel . SetSignalsText ( g_signalsVisible ? " Hide Signals " : " Show Signals " ) ;
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
bool noAI = ( TrainableSignalCount ( ) = = 0 ) ;
2026-07-25 16:39:11 -04:00
//--- The four training buttons describe ONE state machine, so their labels are derived together
//--- rather than independently - otherwise the panel offers actions that silently do nothing.
bool deployed = ! noAI & & AllTrainingDeployed ( ) ;
ExtPanel . SetPauseText ( noAI ? " Pause Training (n/a) "
: deployed ? " Pause Training (deployed) "
: ( AllTrainingPaused ( ) ? " Resume Training " : " Pause Training " ) ) ;
ExtPanel . SetStopText ( noAI ? " Stop Training (n/a) "
: deployed ? " Stop Training (deployed) "
: ( AllTrainingStopped ( ) ? " Start Training " : " Stop Training " ) ) ;
ExtPanel . SetDeployText ( noAI ? " Deploy Model (n/a) " : ( deployed ? " Retrain Model " : " Deploy Model " ) ) ;
2026-07-14 22:36:27 -04:00
ChartRedraw ( 0 ) ;
}
2026-07-26 14:45:08 -04:00
//--- CAppDialog is user-draggable; a drag near an edge followed by shrinking the chart (or dragging
//--- past the visible area) can leave it partially or fully off-screen with no way to grab it back.
//--- Clamps it back inside the current chart bounds whenever the chart is resized/scrolled.
void ClampControlPanelToChart ( void )
{
long chartWidth = ChartGetInteger ( 0 , CHART_WIDTH_IN_PIXELS ) ;
long chartHeight = ChartGetInteger ( 0 , CHART_HEIGHT_IN_PIXELS ) ;
if ( chartWidth < = 0 | | chartHeight < = 0 )
return ;
int x = ExtPanel . Left ( ) ;
int y = ExtPanel . Top ( ) ;
int w = ExtPanel . Width ( ) ;
int h = ExtPanel . Height ( ) ;
int maxX = ( int ) chartWidth - w ;
int maxY = ( int ) chartHeight - h ;
int clampedX = ( maxX < 0 ) ? 0 : MathMin ( MathMax ( x , 0 ) , maxX ) ;
int clampedY = ( maxY < 0 ) ? 0 : MathMin ( MathMax ( y , 0 ) , maxY ) ;
if ( clampedX ! = x | | clampedY ! = y )
ExtPanel . Move ( clampedX , clampedY ) ;
}
2026-07-14 22:36:27 -04:00
//--- creates the control panel dialog once, from OnInit() - the standard CAppDialog usage pattern
2026-08-22 00:25:52 -04:00
//--- (create in OnInit, destroy in OnDeinit; see Controls\Dialog.mqh).
2026-07-14 22:36:27 -04:00
bool CreateControlPanel ( void )
{
ResetLastError ( ) ;
long chartWidth = ChartGetInteger ( 0 , CHART_WIDTH_IN_PIXELS ) ;
int panelX1 = ( chartWidth > CP_PANEL_W + CP_RIGHT_MARGIN + 20 ) ? ( int ) ( chartWidth - CP_PANEL_W - CP_RIGHT_MARGIN ) : 10 ;
2026-08-22 00:25:52 -04:00
//--- Same orphan sweep the signal arrows get, for the same reason: CAppDialog namespaces every
//--- control it creates under the dialog name, those objects live in the CHART PROFILE, and
//--- Destroy() is the only thing that removes them.
fix: purge every EA object namespace on init and after deinit teardown
Leftover objects survived deinit because the cleanup list had drifted.
PurgeChart()'s own comment said it removed "our namespaced signal arrows
plus the status-label objects" while the code removed arrows ONLY, and
the panel prefix was swept at OnInit and nowhere else - so an ordinary
deinit left the status line, and any panel straggler, on the chart.
Three scattered call sites and a comment cannot be kept in step. There is
now ONE list - WarriorChartPrefixes() - covering arrows, status label and
panel, and one sweep, WarriorPurgeChartObjects(), used by every path.
Add a prefix there when a new object family appears and every cleanup
picks it up.
Two call sites added:
OnInit, before ANYTHING is drawn (including the status label it would
otherwise delete). Chart objects live in the chart PROFILE, not in the
EA, so they outlive the process: a deinit force-terminated at
MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5
replaced while attached all strand objects no later deinit will ever
own - and deleting the EA's files does not remove them, which is why
they read as corruption. Arrows are included: LoadChartSignals restores
them from their sidecar moments later and already opens with its own
arrow sweep, so this only removes orphans the sidecar does not account
for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds
that sidecar by scanning the chart.
OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control
tree and ClearStatusLabel clears text rather than guaranteeing object
removal; either can leave a straggler and nothing looked afterwards.
Bounded work - three prefix deletes and one object-list scan - so it
respects the ordering rule that keeps the cheap visible cleanup ahead
of the heavy save. Arrows excluded: ShutdownChartCleanup already
persisted and removed them and re-deleting would race that write.
The two are complementary: the deinit sweep closes the ordinary case, the
OnInit purge closes the case where MetaTrader never let us finish. Only
the second can help after a starved shutdown.
Both sweeps rescan by name across EVERY object type and delete what the
bulk call missed. ObjectsDeleteAll's return has already been observed
disagreeing with a by-name scan of the same chart microseconds apart, and
object commands are queued on the chart rather than applied inline, so a
returned count is not evidence the objects are gone.
Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so
the name cannot drift away from the list that cleans it up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
ObjectsDeleteAll ( 0 , WARRIOR_PANEL_PREFIX ) ;
if ( ! ExtPanel . Create ( 0 , WARRIOR_PANEL_PREFIX , 0 , panelX1 , CP_Y0 , panelX1 + CP_PANEL_W , CP_Y0 + CP_PANEL_H ) )
2026-07-14 22:36:27 -04:00
{
Print ( __FUNCTION__ + " : failed to create control panel, error " + IntegerToString ( GetLastError ( ) ) ) ;
return false ;
}
if ( ! ExtPanel . Run ( ) )
{
Print ( __FUNCTION__ + " : failed to run control panel, error " + IntegerToString ( GetLastError ( ) ) ) ;
return false ;
}
ExtPanel . ForceMaximize ( ) ;
2026-07-26 14:45:08 -04:00
ClampControlPanelToChart ( ) ;
2026-07-25 16:39:11 -04:00
//--- seed the transition watcher (see OnTimer) so a model that is ALREADY deployed at attach time
//--- doesn't register as a fresh transition on the first timer tick
g_lastDeployedState = AllTrainingDeployed ( ) ;
2026-07-14 22:36:27 -04:00
RefreshControlPanelLabels ( ) ;
return true ;
}
2026-07-23 08:48:44 -04:00
//--- Blocking on purpose, unlike the Alert() calls below: this only ever runs in direct response to the
//--- trader clicking a destructive (red) panel button, so pausing for their yes/no is expected UX, not
//--- an unwanted stall of live trade management - MessageBox() briefly blocks the chart's UI thread,
//--- which is exactly what a "are you sure?" gate needs.
bool ConfirmDestructiveAction ( string message )
{
return MessageBox ( message + " \n \n This action cannot be undone. " , " Warrior EA - Confirm " ,
MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2 ) = = IDYES ;
}
2026-08-24 00:41:12 -04:00
//--- One handler per CP_ACTION_* below (called only from HandleControlPanelAction's dispatch table
//--- at the bottom of this block) - each keeps its own guard/confirm/Alert sequence exactly as it was
//--- when all nine lived in one switch, just independently readable/testable now.
void HandleCpToggleSignals ( void )
{
ToggleSignalsVisibility ( ) ;
//--- If a rescan got queued (Show Signals with a deployed model to re-infer from), the
//--- label refresh + Alert are deferred to FinalizeSignalsRescanIfDone() - firing "shown"
//--- here would lie about arrows that don't exist on the chart yet.
if ( ! g_signalsRescanPending )
{
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: signal arrows " + ( g_signalsVisible ? " shown " : " hidden " ) ) ;
}
}
void HandleCpTogglePause ( void )
{
//--- A deployed model has no training run to pause - say so instead of silently doing nothing.
if ( AllTrainingDeployed ( ) )
{
Alert ( " Warrior EA: the model is deployed - there is no training run to pause. \n Use \" Retrain Model \" first if you want to train it further. " ) ;
return ;
}
//--- Direction resolved ONCE here, then handed to every signal as a plain command. A toggle
//--- each model re-derived from its own state is how a mixed set ends up half paused.
bool pause = ! AllTrainingPaused ( ) ;
int touched = Expert . DispatchSignalCommand ( pause ? SIGCMD_PAUSE_TRAINING : SIGCMD_RESUME_TRAINING ) ;
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: training " + ( pause ? " paused " : " resumed " ) +
" ( " + IntegerToString ( touched ) + " model(s)) " ) ;
}
void HandleCpToggleStop ( void )
{
//--- Same as Pause: Stop/Start operate on a training run, and a deployed model isn't one.
//--- Routing this to RetrainDeployed() instead would silently do the Deploy button's job.
if ( AllTrainingDeployed ( ) )
{
Alert ( " Warrior EA: the model is deployed and already running live inference, not training. \n Use \" Retrain Model \" to put it back into training. " ) ;
return ;
}
bool doStop = ! AllTrainingStopped ( ) ;
int touched = Expert . DispatchSignalCommand ( doStop ? SIGCMD_STOP_TRAINING : SIGCMD_START_TRAINING ) ;
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: training " + ( doStop ? " stopped " : " restarted " ) +
" ( " + IntegerToString ( touched ) + " model(s)) " ) ;
}
void HandleCpToggleDeploy ( void )
{
if ( TrainableSignalCount ( ) = = 0 )
{
Alert ( " Warrior EA: no AI signal is active - nothing to deploy (set the AI algorithm input to something other than Disabled). " ) ;
return ;
}
//--- RETRAIN direction: put the finalised model back into training, continuing from its own
//--- weights. Reversible, non-destructive (the weights on disk stay), so no confirmation.
if ( AllTrainingDeployed ( ) )
{
Expert . DispatchSignalCommand ( SIGCMD_RETRAIN_DEPLOYED ) ;
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: retraining the deployed model - it continues from its current weights. \n Use \" Delete & Reset Weights \" instead to start from scratch. " ) ;
return ;
}
//--- DEPLOY direction. The plateau ladder refuses to auto-deploy a model that never cleared the
//--- per-class recall floor (it would be one that ignores Buy or Sell); a manual deploy is the
//--- operator's call, but they should make it knowingly - so this is the one case that asks.
if ( AnyDeployWouldSkipRecallFloor ( ) & &
! ConfirmDestructiveAction ( " No training era has cleared the per-class recall floor yet, so this model may "
" be ignoring Buy or Sell entirely. \n \n Deploy it anyway as the final model? " ) )
{
Alert ( " Warrior EA: deploy cancelled - training continues " ) ;
return ;
}
int deployed = Expert . DispatchSignalCommand ( SIGCMD_DEPLOY ) ;
RefreshControlPanelLabels ( ) ;
if ( deployed = = 0 )
Alert ( " Warrior EA: could not deploy - the AI signal is not initialised yet. " ) ;
else
Alert ( " Warrior EA: model deployed ( " + IntegerToString ( deployed ) + " signal(s)). \n Training stopped; it now runs live inference. Click \" Retrain Model \" to train it further. " ) ;
}
void HandleCpSave ( void )
{
int saved = Expert . DispatchSignalCommand ( SIGCMD_SAVE_WEIGHTS ) ;
Alert ( " Warrior EA: weights saved ( " + IntegerToString ( saved ) + " of " +
IntegerToString ( TrainableSignalCount ( ) ) + " model(s)) " ) ;
}
void HandleCpLoad ( void )
{
Expert . DispatchSignalCommand ( SIGCMD_LOAD_WEIGHTS ) ;
//--- A reload carries the saved file's own "deployed" flag, so it can flip the whole training
//--- state machine (loading a finalised model makes Deploy read "Retrain Model", and Pause/Stop
//--- read "(deployed)"). Resync the labels or the panel would keep offering stale actions.
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: weights reloaded from disk " ) ;
}
void HandleCpReset ( void )
{
//--- CENSUS BEFORE THE ACTION, walked over the SAME tree the reset itself walks - so the
//--- count and the identity lines describe exactly the models about to be wiped and cannot
//--- drift from them.
int trainable = TrainableSignalCount ( ) ;
PrintFormat ( " %s: RESET requested - %d AI signal(s) reachable on this chart: " ,
__FUNCTION__ , trainable ) ;
Expert . DispatchSignalCommand ( SIGCMD_REPORT_IDENTITY ) ;
if ( ! ConfirmDestructiveAction ( " Delete the saved AI weights of all " +
IntegerToString ( trainable ) +
" AI model(s) on this chart and restart training from era 0? " ) )
{
Alert ( " Warrior EA: weights reset cancelled " ) ;
return ;
}
//--- ResetWeights() returns whether the fresh topology was REBUILT, and that return has
//--- been discarded since it was written. Count it, and say so when they disagree.
int resetOk = Expert . DispatchSignalCommand ( SIGCMD_RESET_WEIGHTS ) ;
RefreshControlPanelLabels ( ) ;
PrintFormat ( " %s: RESET complete - %d of %d AI signal(s) rebuilt a fresh topology. " ,
__FUNCTION__ , resetOk , trainable ) ;
if ( trainable = = 0 )
Alert ( " Warrior EA: nothing to reset - no AI signal is registered on this chart. " ) ;
else
if ( resetOk = = trainable )
Alert ( " Warrior EA: weights reset for " + IntegerToString ( resetOk ) +
" model(s) - training restarts from era 0 " ) ;
else
Alert ( " Warrior EA: weights reset INCOMPLETE - " + IntegerToString ( resetOk ) + " of " +
IntegerToString ( trainable ) + " model(s) rebuilt. \n The rest had their files "
" deleted but could not rebuild a topology - see the Experts log. " ) ;
}
void HandleCpReport ( void )
{
if ( ! UseDatabaseRanking )
{
Print ( __FUNCTION__ + " : trade journal report requires \" Weight filters by DB win-rate \" (UseDatabaseRanking) to be enabled " ) ;
Alert ( " Warrior EA: trade journal report requires \" Weight filters by DB win-rate \" to be enabled " ) ;
return ;
}
string reportPath , reportError ;
if ( journal . GenerateReport ( reportPath , reportError ) )
{
Print ( __FUNCTION__ + " : trade journal report ready - " + reportPath ) ;
Alert ( " Warrior EA: trade journal report exported - see the Experts log for the file path " ) ;
}
else
{
Print ( __FUNCTION__ + " : could not generate trade journal report - " + reportError ) ;
Alert ( " Warrior EA: could not export trade journal report - " + reportError ) ;
}
}
//--- separate from HandleCpReset on purpose: resetting AI weights (a routine, frequent action while
//--- tuning) must never cost the trader their accumulated pattern-confidence/trade-journal history,
//--- and vice versa - these are two independent "start fresh" decisions.
void HandleCpResetDb ( void )
{
if ( UseDatabaseRanking )
{
if ( ! ConfirmDestructiveAction ( " Delete the trade-journal and pattern-confidence database? " ) )
{
Alert ( " Warrior EA: database reset cancelled " ) ;
return ;
}
//--- ResetDatabase() returns false when the file could not be deleted or the connection
//--- could not be reopened, and that return was discarded too - the Alert said
//--- "database reset" either way.
bool dbReset = dbm . ResetDatabase ( ) ;
PrintFormat ( " %s: DATABASE RESET %s - one shared trade-journal/pattern-confidence DB serves all "
" %d AI signal(s) on this chart; per-model weights are NOT affected (use Delete && "
" Reset Weights for those). " , __FUNCTION__ ,
( dbReset ? " succeeded " : " FAILED " ) , TrainableSignalCount ( ) ) ;
Alert ( dbReset
? " Warrior EA: database reset "
: " Warrior EA: database reset FAILED - see the Experts log " ) ;
}
else
{
Print ( __FUNCTION__ + " : database reset requires \" Weight filters by DB win-rate \" (UseDatabaseRanking) to be enabled " ) ;
Alert ( " Warrior EA: database reset requires \" Weight filters by DB win-rate \" to be enabled " ) ;
}
}
2026-07-14 22:36:27 -04:00
//--- performs whatever button action ExtPanel recorded (see ConsumeAction() in ControlPanel.mqh);
//--- a no-op when nothing was clicked since the last call
void HandleControlPanelAction ( ENUM_CP_ACTION action )
{
switch ( action )
{
2026-08-24 00:41:12 -04:00
case CP_ACTION_TOGGLE_SIGNALS : HandleCpToggleSignals ( ) ; break ;
case CP_ACTION_TOGGLE_PAUSE : HandleCpTogglePause ( ) ; break ;
case CP_ACTION_TOGGLE_STOP : HandleCpToggleStop ( ) ; break ;
case CP_ACTION_TOGGLE_DEPLOY : HandleCpToggleDeploy ( ) ; break ;
case CP_ACTION_SAVE : HandleCpSave ( ) ; break ;
case CP_ACTION_LOAD : HandleCpLoad ( ) ; break ;
case CP_ACTION_RESET : HandleCpReset ( ) ; break ;
case CP_ACTION_REPORT : HandleCpReport ( ) ; break ;
case CP_ACTION_RESET_DB : HandleCpResetDb ( ) ; break ;
2026-07-14 22:36:27 -04:00
default :
break ;
}
}
// Helper function to pause execution for a random duration between 1 to 3 seconds
void RandomSleep ( )
{
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and
the lattice structure that shape of generator has. Two places here
actually lean on randomness and both were hurt by it:
WEIGHT INIT. Six He/LeCun-uniform sites drew
((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of
~250k weights had only 32768 possible values and thousands of
connections started byte-identical. Breaking that symmetry is the whole
job of random init.
SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand()
draws to reach 30 bits, and its own comment documented the residual
modulo bias it still carried. HQRndUniformI() is rejection-sampled and
exactly uniform, so the splice and the bias note both go.
CHighQualityRand is L'Ecuyer's combined multiplicative congruential
generator - two differenced streams, 31-bit output, period ~2.3e18 -
and it ships with the terminal.
AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount())
calls sit immediately before "build a fresh topology", once per model.
GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every
member inside one OnInit, so members could be handed the SAME seed and
draw the SAME weights wherever their shapes coincide - and members that
start identical are not an ensemble. WarriorRandSeed() takes a salt (the
model id) plus a never-reset call counter, so a collision is impossible
rather than merely unlikely, while the tick keeps the run itself
genuinely unrepeatable the way those call sites asked for.
Seeds are masked positive rather than trusted: HQRndSeed computes
s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the
generator in a state its own assertions reject. GetTickCount() is a uint
and goes negative as an int after ~24 days of uptime - a fault that
would surface as "training is broken" on a long-running terminal and
nowhere else.
The indicator tuner's 52 draws move across too: its random search is
where sample quality earns its keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
Sleep ( WarriorRandInt ( 2000 ) + 1000 ) ; // Sleeps between 1000ms (1s) and 3000ms (3s)
2026-07-14 22:36:27 -04:00
}
2026-07-17 23:21:12 -04:00
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| The DB fingerprint's first slot - was (int)AIType until the |
//| preset selector was replaced by the per-NN toggles (2026-08-19). |
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
//+------------------------------------------------------------------+
int DbLegacyAiSlot ( )
{
int bits = ( Use_MLP ? 1 : 0 ) | ( Use_CONV ? 2 : 0 ) | ( Use_LSTM ? 4 : 0 ) | ( Use_CONVLSTM ? 8 : 0 ) ;
if ( bits = = 15 )
return 6 ; // all four = the old AI_HYBRID ensemble preset
if ( bits = = 0 )
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
return 0 ; // legacy AI_NONE (slot 5 was AI_META, removed 2026-08-25)
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 ( bits = = 1 )
return 1 ; // AI_MLP
if ( bits = = 2 )
return 2 ; // AI_CONV
if ( bits = = 4 )
return 3 ; // AI_LSTM
if ( bits = = 8 )
return 4 ; // AI_CONVLSTM
return 100 + bits ; // new subset - outside the legacy value space by construction
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Config fingerprint appended to the pattern-confidence/trade- |
2026-07-22 22:51:04 -04:00
//| journal database's filename, so a topology or feature-set change |
2026-08-22 00:30:14 -04:00
//| that would produce a differently-shaped/behaving model gets its |
//| own database instead of silently reusing pattern-weight/journal |
//| history that no longer matches what's actually trading now. |
2026-07-22 22:51:04 -04:00
//+------------------------------------------------------------------+
string ComputeDbConfigFingerprint ( )
{
2026-08-22 00:25:52 -04:00
//--- InitialNeurons is gone from this key: the first-layer width is now DERIVED from the input
//--- width and the study period (CExpertSignalAIBase::ComputeFirstLayerWidth), not chosen, and
//--- every determinant of it that IS a user choice is already hashed here.
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 fp = StringFormat ( " SEM%d| " , SIGNAL_DB_SEMANTICS_VERSION )
+ StringFormat ( " %d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d " ,
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
DbLegacyAiSlot ( ) , ( int ) OutputNeuronsCount , ( int ) TrainingOptimizer ,
2026-08-11 21:53:37 -04:00
//--- LEGACY SLOT (was ind_Periods, derived since 2026-08-11). The literal
//--- is the shipped default so every existing database keeps its key.
20 ,
2026-07-22 22:51:04 -04:00
EnableVolume , EnableTime , EnableATR ,
ditch(signals): remove the four classic votes - all 26 patterns measured at chance
research/classic.py transcribed all 26 shipped vote patterns (MA 4, RSI 4, MACD 6,
Ichimoku 12) with their constructor weights and tested them as entries on 178k-bar
histories, four instruments x three barrier geometries. Nothing separated from
chance - not one pattern, not the averaged vote at any threshold 10-70, not a
2/3/4-module quorum, not event-plus-confirmation. Residual E[R] everywhere was
-0.01 to -0.08 R, which is approximately the spread. The +4 sigma reading that had
once justified the set was two bars of lookahead: closing it took MACD_p4 on EURUSD
from +5.05pp to -0.02pp.
All four inputs have shipped false ever since, so this deletes dormant code rather
than changing behaviour.
RETRAIN-NEUTRAL, deliberately. EnableMA and EnableRSI were hashed UNCONDITIONALLY
into the DB config fingerprint, so they become literal 0 legacy slots - the same
treatment the ind_Periods slot two lines above already uses, and every existing
database keeps its key. EnableMACD/EnableIchimoku were appended only when enabled,
so with both gone the segment simply never appears, which is byte-identical to
today. No .nnw or .db is orphaned.
WHAT THIS COSTS, STATED PLAINLY: these four were CSignalMETA's only wired candidate
sources, so the on-chart ladder sweep (BuildCorpusBySweep) now has nothing to sweep
and a META chart is no longer self-contained. That is survivable rather than fatal
because MetaPrepareEra already falls back to CMetaCorpus::LoadLargestOnDisk, and its
own comment names this exact case - "charts whose classic filters are disabled".
Use_MetaLabeling ships false regardless. SignalMETA.mqh is otherwise UNTOUCHED, and
its 26-slot one-hot stays at 26: a tester-built corpus on disk still encodes those
pattern ids, and narrowing the descriptor would invalidate every stored corpus.
Signals/SignalMA.mqh SignalRSI.mqh SignalMACD.mqh SignalIchimoku.mqh deleted
Signals/OscillatorDivergence.mqh deleted - RSI and MACD were its only users
Classic_Shift deleted - the four votes were its only readers
Compile-verified in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline
taken before any edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:08:43 -04:00
//--- LEGACY VOTE SLOTS (were EnableMA / EnableRSI, removed 2026-08-24).
//--- Both shipped false and were hashed UNCONDITIONALLY, so every existing
//--- database is keyed on the 0 they contributed; the literals keep those
//--- keys intact. g_TunedMaPeriod/g_TunedMaType stay live - the MA FEATURE
//--- still runs at the adopted periods, and the DB key must describe those
2026-08-16 15:12:54 -04:00
//--- (new periods = new pattern definitions = fresh win-rate history).
ditch(signals): remove the four classic votes - all 26 patterns measured at chance
research/classic.py transcribed all 26 shipped vote patterns (MA 4, RSI 4, MACD 6,
Ichimoku 12) with their constructor weights and tested them as entries on 178k-bar
histories, four instruments x three barrier geometries. Nothing separated from
chance - not one pattern, not the averaged vote at any threshold 10-70, not a
2/3/4-module quorum, not event-plus-confirmation. Residual E[R] everywhere was
-0.01 to -0.08 R, which is approximately the spread. The +4 sigma reading that had
once justified the set was two bars of lookahead: closing it took MACD_p4 on EURUSD
from +5.05pp to -0.02pp.
All four inputs have shipped false ever since, so this deletes dormant code rather
than changing behaviour.
RETRAIN-NEUTRAL, deliberately. EnableMA and EnableRSI were hashed UNCONDITIONALLY
into the DB config fingerprint, so they become literal 0 legacy slots - the same
treatment the ind_Periods slot two lines above already uses, and every existing
database keeps its key. EnableMACD/EnableIchimoku were appended only when enabled,
so with both gone the segment simply never appears, which is byte-identical to
today. No .nnw or .db is orphaned.
WHAT THIS COSTS, STATED PLAINLY: these four were CSignalMETA's only wired candidate
sources, so the on-chart ladder sweep (BuildCorpusBySweep) now has nothing to sweep
and a META chart is no longer self-contained. That is survivable rather than fatal
because MetaPrepareEra already falls back to CMetaCorpus::LoadLargestOnDisk, and its
own comment names this exact case - "charts whose classic filters are disabled".
Use_MetaLabeling ships false regardless. SignalMETA.mqh is otherwise UNTOUCHED, and
its 26-slot one-hot stays at 26: a tester-built corpus on disk still encodes those
pattern ids, and narrowing the descriptor would invalidate every stored corpus.
Signals/SignalMA.mqh SignalRSI.mqh SignalMACD.mqh SignalIchimoku.mqh deleted
Signals/OscillatorDivergence.mqh deleted - RSI and MACD were its only users
Classic_Shift deleted - the four votes were its only readers
Compile-verified in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline
taken before any edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:08:43 -04:00
0 , g_TunedMaPeriod , g_TunedMaType , 0 , g_TunedRsiPeriod ,
2026-07-22 22:51:04 -04:00
EnableSwingContext , EnableNews ,
ditch(features): remove the eight dead feature groups from the input matrix
RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta,
ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure,
WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a
closed verdict: the three oscillators are the same patterns that measured at chance
as entries, and the Wyckoff family returned zero out-of-sample on five independent
instruments - which is what closed the context score.
RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks
larger than it is. Every removed group contributed `flag ? N : 0` to the input
width, and every flag was false, so the width was ALREADY zero for all eight: no
.nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were
hashed unconditionally and become literal 0 legacy slots (the convention the
m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only
when enabled, so their segments simply never appear - byte-identical to every
fingerprint ever produced, since neither ever shipped on.
CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted
inside every .nnw, and Unflatten() rejects a size mismatch by falling back to
constructor defaults - so dropping the dead fields would silently revert the tuned
MA period of every model on disk while keeping its trained weights. That is the
feature/weight mismatch this project has already paid for twice, and it is not
worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and
read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing
searches them. The class comment says all of this at the declaration.
Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one
indicator now, and a name saying "AD" for the MA handle is the kind of stale label
that gets believed later. Its release-AFTER-recreate ordering is untouched - that
is a documented fix, not bookkeeping.
Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0
baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:21:03 -04:00
//--- LEGACY FEATURE SLOTS (were the five AD/Wyckoff flags, removed
//--- 2026-08-24). Hashed unconditionally and all shipped false, so the
//--- literals below are exactly what every existing database is keyed on.
0 , 0 , 0 )
+ StringFormat ( " |%d|%d|%d|%d " , 0 , 0 ,
EnableMAFeature , 0 ) ; //--- last 0 = former EnableRSIFeature
//--- The MACD and Ichimoku segments were appended ONLY WHEN ENABLED and both flags are now gone, so
//--- they can never appear - which is byte-identical to every fingerprint this has ever produced,
//--- since neither feature has shipped enabled. Nothing is orphaned by their removal.
2026-08-16 15:12:54 -04:00
//--- Alt-data block, conditional like MACD/ICHI: enabling it changes what the model trades on, so it
//--- keys the database; only the FLAG goes in, never the per-symbol feature list - that is a measured
//--- property served by the AltData file and pinned per-model in the .cfg, and a measured quantity
//--- must not key a filename (same rule as the cross-asset pair set).
if ( EnableAltData )
fp + = " |ALT:1 " ;
2026-08-22 00:25:52 -04:00
//--- Cross-asset, conditional for the same reason. Only the flag goes in, never the discovered
//--- reference set - see the matching comment in BuildConfigFingerprint() for why a measured
//--- quantity must not key a filename.
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
if ( EnableCrossAsset )
fp + = StringFormat ( " |XA:%d " , EnableCrossAsset ) ;
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks
Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature,
default on). Spread is the one microstructure channel that is both FX-available and
genuinely historical in the Strategy Tester - "during testing, the spread is not modeled
but is taken from historical data" - so unlike swap, signed tick flow or depth of market it
is something a backtest can honestly validate.
What it encodes, stated precisely because the raw measurement overstates it.
research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5
of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges
the spread inside its own barriers, so a wide-spread bar is mechanically likelier to
resolve as a loss and the feature would partly be predicting its own cost model. Relabelling
at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology
and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime
reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when
realised volatility is below its own ATR estimate, which genuinely predicts whether
ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side.
Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated
in the spread series. Both cached on length alone:
if(m_crossAsset.Bars() >= bars) return true;
MQL5 series indices are relative to NOW, so one new closed candle shifts every index by
one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer
the newest, and every cross-asset value is read one bar out of step with the price features
sitting beside it in the same vector - silently, with no error and no shape change. This is
the same class of defect as the dtStudied watermark behind the zero-direction backtests.
Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the
label/feature bar caches already use.
And a performance fix that fell out of it: with correct invalidation the panel rebuilds on
every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one
full multi-symbol resample per simulated bar at training depth. Inference only reads bars
0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The
cache check is >=, so a deeper panel left from training still satisfies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
if ( EnableSpreadFeature )
fp + = StringFormat ( " |SPR:%d " , EnableSpreadFeature ) ;
2026-07-22 22:51:04 -04:00
uint fpHash = 2166136261 ;
int fpLen = StringLen ( fp ) ;
for ( int fpi = 0 ; fpi < fpLen ; fpi + + )
{
fpHash ^ = ( uint ) StringGetCharacter ( fp , fpi ) ;
fpHash * = 16777619 ;
}
return StringFormat ( " %08x " , fpHash ) ;
}
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
//+------------------------------------------------------------------+
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//| Verifies every trade-management enum input actually holds a |
//| member of its own enum. See the call site in OnInit() for why |
//| this is a hard gate and not a clamp. Returns false (and explains |
//| itself) on any stale value. |
//| |
//| WIDENED 2026-08-25, and this is the reason it had to be: removing |
//| the five confidence-scaled trade-management modes vacated a value |
//| in FOUR enums at once, and MetaTrader validates none of them when |
//| it replays a saved .set or a stored optimization pass. Left |
//| unguarded, a chart saved with the Intelligent stop would have fed |
//| SL_Mode = -1 into a slMultiplier that is now used verbatim - a |
//| stop placed on the WRONG SIDE of the entry, silently, on a live |
//| account. Refusing to start is the only acceptable response to an |
//| input whose meaning changed underneath a saved file. |
//+------------------------------------------------------------------+
bool ValidateTradeManagementInputs ( )
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
{
int sl = ( int ) SL_Mode ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
bool slOk = ( sl = = SL_ATR_x1 | | sl = = SL_ATR_x2 | | sl = = SL_ATR_x3 ) ;
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
int tp = ( int ) TP_Mode ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
bool tpOk = ( tp = = TP_ATR_x1 | | tp = = TP_ATR_x2 | | tp = = TP_ATR_x3 | |
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
tp = = TP_ATR_x4 | | tp = = TP_ATR_x6 | | tp = = TP_ATR_x8 | | tp = = TP_ATR_x10 ) ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
int en = ( int ) Entry_Multiplier ;
bool enOk = ( en = = MARKET | | en = = LIMIT_1xATR | | en = = LIMIT_2xATR | | en = = LIMIT_3xATR | |
en = = STOP_1xATR | | en = = STOP_2xATR | | en = = STOP_3xATR ) ;
int tr = ( int ) TrailingStrategy ;
bool trOk = ( tr = = TRAILING_STRATEGY_NONE | | tr = = TRAILING_STRATEGY_ATR_x1 | |
tr = = TRAILING_STRATEGY_ATR_x2 | | tr = = TRAILING_STRATEGY_ATR_x3 ) ;
int mm = ( int ) MM_STRATEGY ;
bool mmOk = ( mm = = FIXED_RISK | | mm = = FIXED_LOT ) ;
if ( slOk & & tpOk & & enOk & & trOk & & mmOk )
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
return true ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
string bad = " " ;
if ( ! slOk )
bad + = ( bad = = " " ? " " : " , " ) + " Stop-loss mode ( " + IntegerToString ( sl ) + " ) " ;
if ( ! tpOk )
bad + = ( bad = = " " ? " " : " , " ) + " Take-profit mode ( " + IntegerToString ( tp ) + " ) " ;
if ( ! enOk )
bad + = ( bad = = " " ? " " : " , " ) + " Entry type/offset ( " + IntegerToString ( en ) + " ) " ;
if ( ! trOk )
bad + = ( bad = = " " ? " " : " , " ) + " Trailing stop ( " + IntegerToString ( tr ) + " ) " ;
if ( ! mmOk )
bad + = ( bad = = " " ? " " : " , " ) + " MM strategy ( " + IntegerToString ( mm ) + " ) " ;
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
Print ( " Warrior EA: REFUSING TO START - " + bad + " is not one of the available options. " ) ;
Print ( " Warrior EA: this happens when a chart's saved settings were written by an older version of the "
" EA that offered an option which no longer exists. MetaTrader keeps the old value silently. " ) ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
Print ( " Warrior EA: FIX - open the EA's Inputs tab, re-pick the option(s) named above from their "
" dropdowns, then press OK. " ) ;
Alert ( " Warrior EA: " + bad + " is invalid - re-pick it in the Inputs tab. See the Experts log. " ) ;
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon
Three defects found by reading the 2026-08-01 training logs, all of which
only became visible because the relabel made the numbers mean something.
1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET.
`OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101`
-101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in
7eb48f5. MetaTrader does not validate a saved enum input against the
enum's current members, so charts saved before that kept the old
integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;`
then quietly turned it into a 1:1 barrier, and all four topologies
trained ~250 eras against a strategy nobody selected - while the log
reported "target 1.00*ATR" as though it were configured.
Since the relabel these two inputs ARE the label definition, so this
is not a bad trade setting, it is a wrong dataset. ValidateBarrier-
Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix)
on any value that is not an enum member. Members are enumerated rather
than range-checked because both enums are sparse and carry negative
sentinels, so no min/max test can tell a legal value from a deleted
one - which is the entire failure mode. The fallback survives as
belt-and-braces but now announces itself: a fallback that cannot say
it fired is indistinguishable from correct behaviour.
2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE.
`tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct
is Buy+Sell as a share of all bars. At the old exact-pivot target that
was ~6%, so "beat the base rate" read as "beat chance" and the test
looked sound. Triple-barrier labels put it at ~83%, so the gate now
demanded 83% directional precision - impossible by construction.
Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing
safe to deploy" at a perfectly healthy 43-45% precision, with no
checkpoint able to ship however good it got.
Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the
score of the degenerate always-call-one-direction model this floor
exists to reject. Correct at any base rate - ~43% on the current
labels, ~3% on the old rare-pivot ones. The era line now prints
"(chance N%, edge +Mpp)" beside the selection score, because 44%
precision is excellent against a 3% chance level and worthless against
a 43% one, and reading the first as the second is what made tonight's
run look better than it was.
3. THE HORIZON IGNORED THE BARRIER GEOMETRY.
ComputeBarrierHorizonBars() returned the median ZigZag leg, which
measures how long a ~1 ATR move takes and says nothing about how long
the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales
with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled
horizon would have timed out most 1:3 trades and pushed Neutral
straight back up, re-creating the imbalance the relabel removes.
Now multiplied by slMult*tpMult, calibrated against a real measurement
rather than assumed: the accidental 1:1 run resolved at horizon 12 with
only 16.7% timeouts, so the swing median is the right scale at m*k=1.
Verifiable, not just asserted: the prebuild now counts barriers that
ended on the VERTICAL barrier and reports them as a share of Neutral.
Neutral conflates "timed out" with "stopped out" and only the first
indicts the horizon.
Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting
TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right:
no existing model was trained on the intended target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
return false ;
}
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
2026-08-02 12:25:20 -04:00
//| Risk-limit inputs are now free-entry doubles rather than a preset |
//| dropdown, which is what makes them expressive enough for a real |
//| funded-account agreement - and also what makes a typo possible. |
//| These limits gate every trade the EA will ever place, so a wrong |
//| value here is not a suboptimal setting, it is an unprotected |
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//| account. Same reasoning as ValidateTradeManagementInputs(): refuse |
2026-08-02 12:25:20 -04:00
//| start rather than substitute something plausible. |
//+------------------------------------------------------------------+
bool ValidateRiskInputs ( )
{
if ( ! EnableRiskGuard )
return true ;
string bad = " " ;
if ( MaxDailyLossPct < 0.0 | | MaxDailyLossPct > = 100.0 )
bad + = " Daily loss limit ( " + DoubleToString ( MaxDailyLossPct , 2 ) + " %) must be >= 0 and < 100. " ;
if ( MaxDrawdownPct < 0.0 | | MaxDrawdownPct > = 100.0 )
bad + = " Max total drawdown ( " + DoubleToString ( MaxDrawdownPct , 2 ) + " %) must be >= 0 and < 100. " ;
if ( RiskPerTradeOfBudget < = 0.0 | | RiskPerTradeOfBudget > 100.0 )
bad + = " Max % of remaining budget per trade ( " + DoubleToString ( RiskPerTradeOfBudget , 2 ) +
" ) must be > 0 and <= 100. " ;
if ( RiskDayResetHour < 0 | | RiskDayResetHour > 23 )
bad + = " Risk day reset hour ( " + IntegerToString ( RiskDayResetHour ) + " ) must be 0-23. " ;
if ( bad ! = " " )
{
Print ( " Warrior EA: REFUSING TO START - " + bad ) ;
Print ( " Warrior EA: fix the Risk Guard section of the Inputs tab. Enter the limits from your account "
" agreement as plain percentages (e.g. 4 and 8), or 0 to disable a rule. " ) ;
Alert ( " Warrior EA: Risk Guard inputs are invalid - see the Experts log. " ) ;
return false ;
}
//--- Not fatal, but always wrong in practice: a daily allowance at or above the total allowance means
//--- the daily rule can never trip first, so the first thing it ever protects is nothing.
if ( MaxDailyLossPct > 0.0 & & MaxDrawdownPct > 0.0 & & MaxDailyLossPct > = MaxDrawdownPct )
Print ( " Warrior EA: WARNING - daily loss limit ( " , DoubleToString ( MaxDailyLossPct , 2 ) ,
" %) is not tighter than the max drawdown limit ( " , DoubleToString ( MaxDrawdownPct , 2 ) ,
" %). One full daily loss would end the account, so the daily rule protects nothing. " ) ;
if ( ! RiskGuardFlatten )
Print ( " Warrior EA: NOTE - 'Close own positions on breach' is OFF. The risk limits will decline new "
" entries and shrink position sizing, but an ALREADY-OPEN position can still run through the "
" limit - which is how a hard daily loss rule is usually breached. Turn it on for a funded "
" account where a breach ends the account. " ) ;
return true ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Apply the configuration shared by every AI architecture. |
2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
void ConfigureAISignal ( CExpertSignalAIBase * aiSignal )
{
if ( CheckPointer ( aiSignal ) = = POINTER_INVALID )
return ;
aiSignal . OutputNeuronsCount ( OutputNeuronsCount ) ;
2026-08-11 21:53:37 -04:00
//--- HistoryBars is no longer seeded here: the window is DERIVED at InitNeuralNetwork (fresh
//--- model) or ADOPTED from the .cfg (existing model) - see DeriveHistoryBars.
fix(signal): the cooldown belonged at the VOTE layer, as a filter - not per member
cooldown-v1 extended NmsLiveAccept, which declusters each MEMBER's own signal.
That is not what the charts show and not what trades. The combined vote in
CExpertSignalCustom had NO spacing rule at all - grep found not one reference to
the cluster window in that file - so four individually-declustered members were
averaged into a vote that could fire on consecutive bars. Measured live: 2,970
voting bars becoming 299-328 vote arrows.
Proof of the diagnosis, from the deployed fleet under cooldown-v1: SP500 273 and
XAUUSD 212 arrows, unchanged from before the change. The member-level rule could
not touch them.
Gated where the vote becomes a trade - CheckOpenPosition, beside the
open-prohibition and open-market-closed checks, tracing as "open-cooldown". That
is the filter chain the request asked for from the start and it is where this
should have gone first.
Suppression there means no order AND no live arrow, honouring the same "no arrow,
no vote, no position" contract the member rule already had.
THE DRAWN HISTORY NEEDED A SECOND PASS, NOT AN INLINE TEST. The overlay sweep
walks NEWEST->OLDEST and is chunked across ticks, so an inline cooldown would
keep the NEWEST bar of a cluster while the live gate keeps the FIRST, and the
drawn set would contradict the traded set - the exact defect the renderer's own
comments warn about. The sweep now records what it drew and prunes it backwards
over that record, which is forward in time.
Direction() is a TRANSACTION that can run more than once on a bar, so the live
accept is cached per bar time. Without that a second call flips the bar's verdict
after it has already journaled one.
One resolver, WarriorSignalCooldownBars(), now serves both layers so they can
never disagree about the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:29:12 -04:00
aiSignal . SignalClusterWindow ( WarriorSignalCooldownBars ( ) ) ;
feat(signal): make the signal cooldown tunable, and add a hard any-direction gate
The declustering the charts needed already existed - NmsLiveAccept, per-direction
run-collapse plus cross-direction resolution plus strict alternation - and it was
already set to 10 bars. It could not be TUNED: SignalClusterWindow was a compile-
time const, so finding the right value needed a rebuild. That is the actual gap.
Now three inputs, as enum dropdowns:
Signal_CooldownScope per-direction, or a hard any-direction gate on top
Signal_CooldownBars SCB_OFF..SCB_50, default 10
Signal_CooldownMinutes SCM_OFF..SCM_1440, overrides bars when set
Minutes resolve against the CHART period and round UP, so a cooldown asked for in
wall-clock is never silently shorter than requested and survives a timeframe
change.
SCB_/SCM_ prefixes are deliberately unique. M15/M30/M60 are ALREADY members of
NF_LOOKBACK_PRESETS, and MQL5 binds a duplicated enum member to the first-declared
enum silently - the obvious names would have compiled straight into the news
filter's values.
THE ANY-DIRECTION GATE IS ADDITIVE, NOT A REPLACEMENT, and the first cut of this
had it backwards. Measured on the live log: the current rules draw 222 arrows over
4999 bars, while a BARE 10-bar cooldown permits up to 454 - because ALTERNATION is
what declutters today, not the window. Swapping the rules out would have roughly
doubled the clutter it was asked to remove. Layered, it can only ever suppress
more. Suppressed bars still advance the per-direction last-SEEN cursors, so a run
straddling the boundary does not restart as if it were fresh.
Applied at all THREE sites that must agree - live inference, OOS pass-3 scoring
and the chart renderer. Their own comments say why: an arrow set that does not
obey the same rule as the traded set shows calls the EA would never take.
Also corrects a stale comment that called this window "display only". It is not:
when it suppresses, the live path zeroes the signal outright - no arrow, no vote,
no position. Training never sees it, so these cost no retrain and are correctly
absent from the fingerprint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 09:48:28 -04:00
aiSignal . SignalCooldownScope ( Signal_CooldownScope ) ;
2026-08-01 11:27:28 -04:00
aiSignal . FreezePriorCalibration ( FreezePriorCalibration ) ;
aiSignal . SwingConfirmationBars ( SwingConfirmationBars ) ;
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
if ( ( EnablePAI ? 1 : 0 ) + ( EnableCONV ? 1 : 0 ) + ( EnableLSTM ? 1 : 0 ) + ( EnableHYBRID ? 1 : 0 ) > = 2 )
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:
1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
id 1 and handled id 1001, and CExpertCustom broadcasts every chart
event to every filter - so each posted event ran a train chunk in ALL
N members (N*N chunks per round) and the chart thread never idled
long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
OnChartEventHandler. Fix: per-instance study-event ids
(STUDY_EVENT_ID_BASE + construction order, offset above the Controls
library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
double-clicks fired training chunks). ArmStudyEvent() is the single
post site; lost-event watchdog replaces the accidental
sibling-clears-my-flag rescue.
2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
identical features/labels, and it ends in the full MI diagnostic
suite, which the MI-share gate never intercepted on the sweep path -
four members ran four identical ~36s sweep+report blocks. First
member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
the rest apply it and skip both.
3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
autosave in flight ate it, OnDeinit got ~430ms and died in the first
member's arrow persist ("Abnormal termination" 432ms in). Fix: early
visible-UI sweep (native prefix deletes for status/panel/dialog)
right after ClearStatusLabel, and a fast path for still-training
models - their arrows are re-rendered every era, so they get one bulk
purge instead of scan+atomic-write in the death window.
4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
by era together; a member ahead of the slowest still-training member
declines Train() calls and its chunk budget is donated
(TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
the last member to finish the era scores the averaged vote vs the
mirrored Min_Vote_Open against the same target-before-stop outcomes
members grade themselves on, publishing an "Ensemble vote" line on
the aggregated panel. Member headlines now carry their lifetime win
rate with break-even.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
//--- second argument mirrors the open threshold into the combined-vote OOS scorer so the
//--- ensemble panel's "Ensemble vote" line fires on the same criterion the live trade does
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
aiSignal . EnsembleMember ( true , ( double ) Signal_ThresholdOpen ) ;
2026-08-01 11:27:28 -04:00
aiSignal . EnableOnlineLearning ( EnableOnlineLearning ) ;
aiSignal . MaxErasPerRun ( MaxErasPerRun ) ;
aiSignal . OOSSplit ( OOSSplit ) ;
if ( ! UseDatabaseRanking )
aiSignal . Weight ( 1 ) ;
aiSignal . UseVolumes ( EnableVolume ) ;
aiSignal . UseTime ( EnableTime ) ;
aiSignal . UseATR ( EnableATR ) ;
aiSignal . UseMA ( EnableMAFeature ) ;
aiSignal . UseSwingContext ( EnableSwingContext ) ;
aiSignal . UseNews ( EnableNews ) ;
aiSignal . NewsFeatureWindowMinutes ( NewsFeatureWindowMinutes ) ;
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
aiSignal . UseCrossAsset ( EnableCrossAsset ) ;
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks
Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature,
default on). Spread is the one microstructure channel that is both FX-available and
genuinely historical in the Strategy Tester - "during testing, the spread is not modeled
but is taken from historical data" - so unlike swap, signed tick flow or depth of market it
is something a backtest can honestly validate.
What it encodes, stated precisely because the raw measurement overstates it.
research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5
of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges
the spread inside its own barriers, so a wide-spread bar is mechanically likelier to
resolve as a loss and the feature would partly be predicting its own cost model. Relabelling
at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology
and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime
reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when
realised volatility is below its own ATR estimate, which genuinely predicts whether
ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side.
Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated
in the spread series. Both cached on length alone:
if(m_crossAsset.Bars() >= bars) return true;
MQL5 series indices are relative to NOW, so one new closed candle shifts every index by
one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer
the newest, and every cross-asset value is read one bar out of step with the price features
sitting beside it in the same vector - silently, with no error and no shape change. This is
the same class of defect as the dtStudied watermark behind the zero-direction backtests.
Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the
label/feature bar caches already use.
And a performance fix that fell out of it: with correct invalidation the panel rebuilds on
every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one
full multi-symbol resample per simulated bar at training depth. Inference only reads bars
0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The
cache check is >=, so a deeper panel left from training still satisfies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
aiSignal . UseSpreadFeature ( EnableSpreadFeature ) ;
2026-08-16 15:12:54 -04:00
aiSignal . UseAltData ( EnableAltData ) ;
2026-08-01 11:27:28 -04:00
aiSignal . AutoTuneIndicators ( AutoTuneIndicators ) ;
}
//+------------------------------------------------------------------+
2026-07-14 22:36:27 -04:00
// Helper function to retry signal creation with error handling
template < typename TSignal >
TSignal * CreateSignalWithRetry ( int maxRetries , bool enableFlag )
{
if ( ! enableFlag )
return NULL ;
TSignal * signal = NULL ;
for ( int tries = 0 ; tries < maxRetries ; + + tries )
{
signal = new TSignal ;
if ( signal = = NULL )
{
Print ( " Initialization of signal failed, retrying... " ) ;
RandomSleep ( ) ;
}
else
break ;
}
if ( signal = = NULL )
{
Print ( " Failed to create and initialize signal after retries " ) ;
}
return signal ;
}
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
//+------------------------------------------------------------------+
2026-08-24 02:07:33 -04:00
// Helper: new + null-check + Expert.InitMoney() + null-check, shared by
// every MM_STRATEGY branch in InitializeMoneyManagement().
template < typename TMoney >
TMoney * CreateAndInitMoney ( const string functionName )
{
TMoney * money = new TMoney ;
if ( money = = NULL )
{
Print ( functionName + " : error creating money " ) ;
return NULL ;
}
if ( ! Expert . InitMoney ( money ) )
{
Print ( functionName + " : error initializing money " ) ;
return NULL ;
}
return money ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| ONE RETRY-AND-REPORT for the init steps that can lose a race |
//| with the terminal (indicator handles, timer registration, the |
//| trade objects' own setup). |
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
//+------------------------------------------------------------------+
typedef bool ( * TInitStep ) ( void ) ;
bool RetryInitStep ( TInitStep step , const string what , const int maxRetries , const string caller )
{
for ( int tries = 0 ; tries < maxRetries ; + + tries )
{
if ( step ( ) )
return true ;
2026-08-21 15:12:23 -04:00
//--- A permanent refusal gives the same answer five times, and the retries push the one line
//--- that explains it off the top of the operator's log. Stop and repeat the reason instead.
if ( g_initFatalReason ! = " " )
{
Print ( caller + " : Failed to " + what + " - " + g_initFatalReason +
" . Retrying cannot change this; see the REFUSED line above. " ) ;
return false ;
}
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
Print ( caller + " : Failed to " + what + " , retrying... " ) ;
RandomSleep ( ) ;
}
Print ( caller + " : Failed to " + what + " after retries " ) ;
return false ;
}
//--- The four steps above, each as the no-argument call RetryInitStep takes. Thin by necessity:
//--- MQL5 function pointers cannot bind a method call or an argument, and these are two of each.
feat(magic): assign the magic number once, then remember it
Expert_MagicNumber = 0 (the new default) means "draw one and write it down".
On first attach the EA picks a random magic in a distinctive band, persists it
to MQL5\Files\Warrior_<symbol>_<period>.magic, and reads that same value back on
every later start. Unique without anyone typing it, and STABLE.
Stability is the whole point. The magic is how the EA recognises its own
positions - a fresh one per start would leave every open position invisible to
the scheduled close-all, the risk-budget flatten and the journal's MAE/MFE walk:
trades still running that no code would ever manage again. So the value is
persisted before it is ever used to trade.
Stored TERMINAL-LOCAL rather than in Common\Files\Warrior_EA, on purpose: that
folder is the one wiped for a retrain, and positions outlive retrains. It also
gives two terminals on the same symbol different magics, which a chart-identity
hash could not.
Fallbacks, both of which stay stable without a file:
* tester/optimizer/forward use a magic derived from chart identity, so two
identical passes cannot differ.
* an unwritable file falls back to that same derived value, and says so.
Books occupy EVEN slots only, so one chart's short book (base+1) can never land
on another chart's long book.
WarriorOwnsMagic() now also recognises the legacy 2024/2025 pair permanently.
Without it, switching an existing chart to 0 while a position was open would
orphan that position. Every caller also matches the symbol, so claiming those
values can only reach positions on this EA's own chart.
Existing charts are untouched: MT5 stores inputs per chart, so the six live
charts keep the 2024 they already have and keep managing what they hold.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:35:58 -04:00
//--- WarriorBookMagic(true), not the raw input: with Expert_MagicNumber at 0 the real magic is the
//--- assigned-and-remembered one, and m_magic must be the LONG book so every inherited CExpert path
//--- addresses the same book SelectPosition() defaults to.
bool StepExpertInit ( void ) { return Expert .Init ( Symbol ( ) , Period ( ) , Expert_EveryTick , WarriorBookMagic ( true ) ) ; }
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
bool StepInitTrailing ( void ) { return InitializeTrailing ( ) ; }
bool StepInitMoneyManagement ( void ) { return InitializeMoneyManagement ( ) ; }
bool StepValidateSettings ( void ) { return Expert . ValidationSettings ( ) ; }
bool StepInitIndicators ( void ) { return Expert . InitIndicators ( ) ; }
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
//--- 500ms: Train() does at most era.budgetMs of work per call, then yields back here.
//--- EventSetMillisecondTimer is needed for sub-second resolution; EventSetTimer takes whole
//--- seconds only.
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
# define WARRIOR_TIMER_INTERVAL_MS 500
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
//+------------------------------------------------------------------+
//| NO SUB-SECOND TIMER IN THE TESTER. The tester fires OnTimer on |
//| SIMULATED time, so a 500ms timer over a 2016-2026 pass is |
//| ~600 MILLION OnTimer calls - each walking 4x PollTraining, the |
//| vote readout's string build, the overlay advance and the deployed |
//| census - none of which an inference-only pass needs: training |
//| never runs (m_inferenceOnly), inference is driven per bar by |
//| OnTickHandler off the tick stream, the risk budget is re-checked |
//| in OnTick, and there is no chart, panel or overlay to keep fresh. |
//| Measured 2026-08-25: 12 agents, 78 minutes, ZERO of 39 passes |
//| finished ("0.1% an hour"); the timer flood was the largest single |
//| consumer left after the optcache/DB/deinit fixes. An hourly |
//| EventSetTimer stays armed as a belt: anything that genuinely |
//| needs an occasional timer still gets ~one call per simulated hour |
//| (~2,600 per pass) instead of six hundred million. |
//+------------------------------------------------------------------+
bool StepSetTimer ( void )
{
if ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) )
return EventSetTimer ( 3600 ) ;
return EventSetMillisecondTimer ( WARRIOR_TIMER_INTERVAL_MS ) ;
}
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
2026-07-14 22:36:27 -04:00
//+------------------------------------------------------------------+
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
//| OnInit PHASES below, in the exact order OnInit calls them. Split |
//| out of one ~430-line function per the DRY/KISS/SOLID sweep - each|
//| is one boot concern, called once, in the order the comments in |
//| OnInit's own body require (alt-data/cross-asset before any model |
//| build, filters added exactly once before the DB retry loop, |
//| etc.) - see OnInit for the call sequence and why it is fixed. |
2026-07-16 01:12:37 -04:00
//+------------------------------------------------------------------+
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//+------------------------------------------------------------------+
//| Delete ORPHANED CONTROL PANELS. CAppDialog names every one of its |
//| objects <numeric instance id><control name> - NOT with a Warrior |
//| prefix - so a force-killed OnDeinit (MetaTrader's ~4,500 ms |
//| budget) strands a complete 15-object panel that no prefix sweep |
//| can ever match, and a re-attach mints a NEW instance id, so the |
//| ghost is permanent. Observed 2026-08-25 18:23: two killed charts |
//| each kept a full "17984Back...17984ResetDB" set, and XTIUSD had |
//| carried a "29641*" set across sessions. |
//| |
//| SAFETY: a numeric prefix qualifies only when at least FOUR of OUR |
//| button names (CreateButtons, Panel\ControlPanel.mqh) carry it - a |
//| dead panel always carries all nine, while a foreign CAppDialog |
//| would have to coincide on four of them (Save/Load alone is |
//| plausible; Save+Load+Pause+Deploy is not) - so another EA's or an |
//| indicator's panel on the same chart is never deleted by name |
//| coincidence. |
//+------------------------------------------------------------------+
int PurgeOrphanedPanelObjects ( void )
{
string buttons [ 9 ] = { " Report " , " Signals " , " Pause " , " Stop " , " Deploy " , " Save " , " Load " , " Reset " , " ResetDB " } ;
string chrome [ 6 ] = { " Back " , " Border " , " Caption " , " ClientBack " , " Close " , " MinMax " } ;
//--- Pass 1: find the numeric prefixes that own our buttons. Bounded small: a chart carries at
//--- most a handful of dead panels, one per kill.
string prefixes [ ] ;
int hits [ ] ;
int nPrefixes = 0 ;
int total = ObjectsTotal ( 0 , -1 , -1 ) ;
for ( int i = 0 ; i < total ; i + + )
{
string nm = ObjectName ( 0 , i , -1 , -1 ) ;
for ( int b = 0 ; b < 9 ; b + + )
{
int cut = StringLen ( nm ) - StringLen ( buttons [ b ] ) ;
//--- Exact-suffix test; cut > 0 also rejects a bare button name with no prefix at all.
if ( cut < = 0 | | StringSubstr ( nm , cut ) ! = buttons [ b ] )
continue ;
string pre = StringSubstr ( nm , 0 , cut ) ;
bool numeric = true ;
for ( int k = 0 ; k < StringLen ( pre ) & & numeric ; k + + )
{
ushort c = StringGetCharacter ( pre , k ) ;
if ( c < ' 0 ' | | c > ' 9 ' )
numeric = false ;
}
if ( ! numeric )
continue ;
int slot = -1 ;
for ( int p = 0 ; p < nPrefixes ; p + + )
if ( prefixes [ p ] = = pre )
{
slot = p ;
break ;
}
if ( slot < 0 )
{
ArrayResize ( prefixes , nPrefixes + 1 ) ;
ArrayResize ( hits , nPrefixes + 1 ) ;
prefixes [ nPrefixes ] = pre ;
hits [ nPrefixes ] = 0 ;
slot = nPrefixes + + ;
}
hits [ slot ] + + ;
break ; // one suffix match per object ("...Reset" cannot also end "...ResetDB")
}
}
//--- Pass 2: for every qualifying prefix, delete the full known object set by NAME. ObjectDelete
//--- on a missing name is a silent no-op, so a partially-created ghost costs nothing extra.
int removed = 0 ;
for ( int p = 0 ; p < nPrefixes ; p + + )
{
if ( hits [ p ] < 4 )
continue ;
for ( int b = 0 ; b < 9 ; b + + )
if ( ObjectDelete ( 0 , prefixes [ p ] + buttons [ b ] ) )
removed + + ;
for ( int c = 0 ; c < 6 ; c + + )
if ( ObjectDelete ( 0 , prefixes [ p ] + chrome [ c ] ) )
removed + + ;
PrintFormat ( " Warrior: removed a dead control panel (instance id %s) - CAppDialog objects carry a "
" numeric id, not a Warrior prefix, so only this by-name pass can reach one that "
" survived a force-killed deinit. " , prefixes [ p ] ) ;
}
return removed ;
}
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
//+------------------------------------------------------------------+
//| Sweep every EA-owned object namespace off the chart (chart |
//| objects outlive the process - see OnInit's own comment on why |
//| this runs first), then report what still doesn't match any of |
//| our prefixes so drift is visible, not just a bare count. |
//+------------------------------------------------------------------+
void PurgeStaleChartObjectsAndReport ( const string caller )
2026-07-14 22:36:27 -04:00
{
fix: purge every EA object namespace on init and after deinit teardown
Leftover objects survived deinit because the cleanup list had drifted.
PurgeChart()'s own comment said it removed "our namespaced signal arrows
plus the status-label objects" while the code removed arrows ONLY, and
the panel prefix was swept at OnInit and nowhere else - so an ordinary
deinit left the status line, and any panel straggler, on the chart.
Three scattered call sites and a comment cannot be kept in step. There is
now ONE list - WarriorChartPrefixes() - covering arrows, status label and
panel, and one sweep, WarriorPurgeChartObjects(), used by every path.
Add a prefix there when a new object family appears and every cleanup
picks it up.
Two call sites added:
OnInit, before ANYTHING is drawn (including the status label it would
otherwise delete). Chart objects live in the chart PROFILE, not in the
EA, so they outlive the process: a deinit force-terminated at
MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5
replaced while attached all strand objects no later deinit will ever
own - and deleting the EA's files does not remove them, which is why
they read as corruption. Arrows are included: LoadChartSignals restores
them from their sidecar moments later and already opens with its own
arrow sweep, so this only removes orphans the sidecar does not account
for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds
that sidecar by scanning the chart.
OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control
tree and ClearStatusLabel clears text rather than guaranteeing object
removal; either can leave a straggler and nothing looked afterwards.
Bounded work - three prefix deletes and one object-list scan - so it
respects the ordering rule that keeps the cheap visible cleanup ahead
of the heavy save. Arrows excluded: ShutdownChartCleanup already
persisted and removed them and re-deleting would race that write.
The two are complementary: the deinit sweep closes the ordinary case, the
OnInit purge closes the case where MetaTrader never let us finish. Only
the second can help after a starved shutdown.
Both sweeps rescan by name across EVERY object type and delete what the
bulk call missed. ObjectsDeleteAll's return has already been observed
disagreeing with a by-name scan of the same chart microseconds apart, and
object commands are queued on the chart rather than applied inline, so a
returned count is not evidence the objects are gone.
Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so
the name cannot drift away from the list that cleans it up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
int initLeftover = 0 ;
int initPurged = WarriorPurgeChartObjects ( 0 , false , initLeftover ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//--- The one family the prefix sweep cannot reach - see PurgeOrphanedPanelObjects. Runs before
//--- the residue report below so a dead panel is removed rather than listed as "not ours".
initPurged + = PurgeOrphanedPanelObjects ( ) ;
fix: purge every EA object namespace on init and after deinit teardown
Leftover objects survived deinit because the cleanup list had drifted.
PurgeChart()'s own comment said it removed "our namespaced signal arrows
plus the status-label objects" while the code removed arrows ONLY, and
the panel prefix was swept at OnInit and nowhere else - so an ordinary
deinit left the status line, and any panel straggler, on the chart.
Three scattered call sites and a comment cannot be kept in step. There is
now ONE list - WarriorChartPrefixes() - covering arrows, status label and
panel, and one sweep, WarriorPurgeChartObjects(), used by every path.
Add a prefix there when a new object family appears and every cleanup
picks it up.
Two call sites added:
OnInit, before ANYTHING is drawn (including the status label it would
otherwise delete). Chart objects live in the chart PROFILE, not in the
EA, so they outlive the process: a deinit force-terminated at
MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5
replaced while attached all strand objects no later deinit will ever
own - and deleting the EA's files does not remove them, which is why
they read as corruption. Arrows are included: LoadChartSignals restores
them from their sidecar moments later and already opens with its own
arrow sweep, so this only removes orphans the sidecar does not account
for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds
that sidecar by scanning the chart.
OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control
tree and ClearStatusLabel clears text rather than guaranteeing object
removal; either can leave a straggler and nothing looked afterwards.
Bounded work - three prefix deletes and one object-list scan - so it
respects the ordering rule that keeps the cheap visible cleanup ahead
of the heavy save. Arrows excluded: ShutdownChartCleanup already
persisted and removed them and re-deleting would race that write.
The two are complementary: the deinit sweep closes the ordinary case, the
OnInit purge closes the case where MetaTrader never let us finish. Only
the second can help after a starved shutdown.
Both sweeps rescan by name across EVERY object type and delete what the
bulk call missed. ObjectsDeleteAll's return has already been observed
disagreeing with a by-name scan of the same chart microseconds apart, and
object commands are queued on the chart rather than applied inline, so a
returned count is not evidence the objects are gone.
Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so
the name cannot drift away from the list that cleans it up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
if ( initPurged > 0 )
PrintFormat ( " %s: chart purge on init - removed %d leftover EA object(s)%s. Chart objects survive a "
" starved deinit, a crash and an .ex5 swap, so a clean start is asserted here rather "
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
" than assumed from the last shutdown. " , caller , initPurged ,
fix: purge every EA object namespace on init and after deinit teardown
Leftover objects survived deinit because the cleanup list had drifted.
PurgeChart()'s own comment said it removed "our namespaced signal arrows
plus the status-label objects" while the code removed arrows ONLY, and
the panel prefix was swept at OnInit and nowhere else - so an ordinary
deinit left the status line, and any panel straggler, on the chart.
Three scattered call sites and a comment cannot be kept in step. There is
now ONE list - WarriorChartPrefixes() - covering arrows, status label and
panel, and one sweep, WarriorPurgeChartObjects(), used by every path.
Add a prefix there when a new object family appears and every cleanup
picks it up.
Two call sites added:
OnInit, before ANYTHING is drawn (including the status label it would
otherwise delete). Chart objects live in the chart PROFILE, not in the
EA, so they outlive the process: a deinit force-terminated at
MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5
replaced while attached all strand objects no later deinit will ever
own - and deleting the EA's files does not remove them, which is why
they read as corruption. Arrows are included: LoadChartSignals restores
them from their sidecar moments later and already opens with its own
arrow sweep, so this only removes orphans the sidecar does not account
for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds
that sidecar by scanning the chart.
OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control
tree and ClearStatusLabel clears text rather than guaranteeing object
removal; either can leave a straggler and nothing looked afterwards.
Bounded work - three prefix deletes and one object-list scan - so it
respects the ordering rule that keeps the cheap visible cleanup ahead
of the heavy save. Arrows excluded: ShutdownChartCleanup already
persisted and removed them and re-deleting would race that write.
The two are complementary: the deinit sweep closes the ordinary case, the
OnInit purge closes the case where MetaTrader never let us finish. Only
the second can help after a starved shutdown.
Both sweeps rescan by name across EVERY object type and delete what the
bulk call missed. ObjectsDeleteAll's return has already been observed
disagreeing with a by-name scan of the same chart microseconds apart, and
object commands are queued on the chart rather than applied inline, so a
returned count is not evidence the objects are gone.
Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so
the name cannot drift away from the list that cleans it up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
( initLeftover > 0
? StringFormat ( " (%d of them needed a by-name delete after the bulk call) " , initLeftover )
: " " ) ) ;
2026-08-22 00:25:52 -04:00
//--- AND SAY WHAT SURVIVED IT. Names, not just a count - a count cannot be acted on. Reporting is
//--- the whole intervention. "Removed N, zero by-name leftovers" only ever meant "nothing matching
//--- OUR PREFIXES remains" - it was never a statement about the chart, and on 2026-08-17 22:00 all
//--- three charts printed exactly that and still came up with duplicated panels.
fix(chart): a purge that reports "zero leftovers" was only ever checking its own list
2026-08-17 21:58: all three charts hit "Abnormal termination" ~5.3 s into
OnDeinit with NO cleanup-timings line - the teardown was starved again. The
22:00 init purge then removed 993 / 1373 / 1557 stranded objects and reported
ZERO by-name leftovers on every chart, and the charts still came up with
duplicated panels. "Nothing matching our prefixes remains" and "the chart is
clean" are different statements and only the first was being made.
Three changes, in the order they matter:
1. WHY the teardown starved, and it is a gap in ad80e0b. StartLabelCachePrebuild
runs ResizeBuffers + RefreshData over the FULL study window (33,984 bars on
XAUUSD), unchunked, and OnDeinit cannot begin until it returns. Normally a
once-per-run cost. That night SP500 and XAUUSD LSTM were wedged in the "cache
invalidated at era start" loop, which calls it on EVERY Train() call - two
members re-preparing tens of thousands of bars indefinitely. The terminal
closed into that. Guarded now, plus a resumable guard in the prebuild chunk
loop (the tally pass after it is not chunked).
2. Catch-all "Warrior" prefix in WarriorChartPrefixes. Every family this EA
creates is named Warrior* except the arrows (WarSig_), so one bare prefix
covers the three named entries AND anything a rename or a stale .ex5 left
under a name nobody remembers. Still a prefix delete, never
ObjectsDeleteAll(chart) - the user's own drawings are not ours to remove. Does
not defeat skipArrows: "WarSig_" does not start with "Warrior".
3. The init purge now REPORTS the residue it did not claim, by name (up to 12).
Not deleted - an unmatched object may belong to the user or another indicator.
If a Warrior panel is visible and appears in neither the removed count nor
this list, the prefix list has drifted a third time and the name is in the
journal instead of being inferred from a screenshot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:07:09 -04:00
{
string resPrefixes [ ] ;
int np = WarriorChartPrefixes ( resPrefixes ) ;
int resTotal = ObjectsTotal ( 0 , -1 , -1 ) ;
string residue = " " ;
int unmatched = 0 ;
for ( int i = 0 ; i < resTotal ; i + + )
{
string nm = ObjectName ( 0 , i , -1 , -1 ) ;
bool ours = false ;
for ( int q = 0 ; q < np ; q + + )
if ( StringFind ( nm , resPrefixes [ q ] ) = = 0 )
{
ours = true ;
break ;
}
if ( ours )
continue ;
unmatched + + ;
if ( unmatched < = 12 )
residue + = ( residue = = " " ? " " : " , " ) + nm ;
}
if ( unmatched > 0 )
PrintFormat ( " %s: chart residue after the init purge - %d object(s) this EA did not create and did "
" not touch: %s%s. If a Warrior panel or status line is VISIBLE on the chart and is "
" not in this list and was not removed above, the prefix list has drifted again "
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
" (see WarriorChartPrefixes). " , caller , unmatched , residue ,
fix(chart): a purge that reports "zero leftovers" was only ever checking its own list
2026-08-17 21:58: all three charts hit "Abnormal termination" ~5.3 s into
OnDeinit with NO cleanup-timings line - the teardown was starved again. The
22:00 init purge then removed 993 / 1373 / 1557 stranded objects and reported
ZERO by-name leftovers on every chart, and the charts still came up with
duplicated panels. "Nothing matching our prefixes remains" and "the chart is
clean" are different statements and only the first was being made.
Three changes, in the order they matter:
1. WHY the teardown starved, and it is a gap in ad80e0b. StartLabelCachePrebuild
runs ResizeBuffers + RefreshData over the FULL study window (33,984 bars on
XAUUSD), unchunked, and OnDeinit cannot begin until it returns. Normally a
once-per-run cost. That night SP500 and XAUUSD LSTM were wedged in the "cache
invalidated at era start" loop, which calls it on EVERY Train() call - two
members re-preparing tens of thousands of bars indefinitely. The terminal
closed into that. Guarded now, plus a resumable guard in the prebuild chunk
loop (the tally pass after it is not chunked).
2. Catch-all "Warrior" prefix in WarriorChartPrefixes. Every family this EA
creates is named Warrior* except the arrows (WarSig_), so one bare prefix
covers the three named entries AND anything a rename or a stale .ex5 left
under a name nobody remembers. Still a prefix delete, never
ObjectsDeleteAll(chart) - the user's own drawings are not ours to remove. Does
not defeat skipArrows: "WarSig_" does not start with "Warrior".
3. The init purge now REPORTS the residue it did not claim, by name (up to 12).
Not deleted - an unmatched object may belong to the user or another indicator.
If a Warrior panel is visible and appears in neither the removed count nor
this list, the prefix list has drifted a third time and the name is in the
journal instead of being inferred from a screenshot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:07:09 -04:00
( unmatched > 12 ? StringFormat ( " ... and %d more " , unmatched - 12 ) : " " ) ) ;
}
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
}
//+------------------------------------------------------------------+
//| Configure the account loss budget BEFORE anything can size or |
//| place a trade. Everything downstream (the money manager's clamp, |
//| the risk-guard veto) reads this one object, so it must be live |
//| from the first tick rather than from whenever the signal |
//| pipeline happens to initialise. |
//+------------------------------------------------------------------+
void ConfigureRiskBudget ( void )
{
2026-08-02 12:25:20 -04:00
g_riskBudget . Configure ( EnableRiskGuard , MaxDailyLossPct , MaxDrawdownPct , MaxDrawdownIsTrailing ,
RiskDayResetHour , RiskPerTradeOfBudget , RiskGuardFlatten ,
feat(magic): assign the magic number once, then remember it
Expert_MagicNumber = 0 (the new default) means "draw one and write it down".
On first attach the EA picks a random magic in a distinctive band, persists it
to MQL5\Files\Warrior_<symbol>_<period>.magic, and reads that same value back on
every later start. Unique without anyone typing it, and STABLE.
Stability is the whole point. The magic is how the EA recognises its own
positions - a fresh one per start would leave every open position invisible to
the scheduled close-all, the risk-budget flatten and the journal's MAE/MFE walk:
trades still running that no code would ever manage again. So the value is
persisted before it is ever used to trade.
Stored TERMINAL-LOCAL rather than in Common\Files\Warrior_EA, on purpose: that
folder is the one wiped for a retrain, and positions outlive retrains. It also
gives two terminals on the same symbol different magics, which a chart-identity
hash could not.
Fallbacks, both of which stay stable without a file:
* tester/optimizer/forward use a magic derived from chart identity, so two
identical passes cannot differ.
* an unwritable file falls back to that same derived value, and says so.
Books occupy EVEN slots only, so one chart's short book (base+1) can never land
on another chart's long book.
WarriorOwnsMagic() now also recognises the legacy 2024/2025 pair permanently.
Without it, switching an existing chart to 0 while a position was open would
orphan that position. Every caller also matches the symbol, so claiming those
values can only reach positions on this EA's own chart.
Existing charts are untouched: MT5 stores inputs per chart, so the six live
charts keep the 2024 they already have and keep managing what they hold.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:35:58 -04:00
( long ) WarriorBookMagic ( true ) , Symbol ( ) ) ;
feat: expectancy stop - halt when the measured result says the strategy loses
The daily (4%) and total (8%) rules bound how FAST an account can lose. Nothing
noticed WHETHER it was losing. A negative-expectancy signal traded at 1% inside
that envelope breaches no rule and still arrives at zero - it just takes longer,
with every limit green the whole way down. That is the realistic way this EA
destroys an account, and no existing guard could see it.
THE ARITHMETIC THIS ENFORCES. Expected value per trade is p*TP - (1-p)*SL - cost.
With no directional edge p equals SL/(SL+TP), which is also the break-even rate,
so the payoff terms cancel exactly and EV = -cost. Expected P&L is -(trades) x
cost: strictly negative, proportional to activity. Measured here: directional
precision 23-24% against a 25% break-even, flat across every confidence tier,
with 58 points of spread on SP500. Sizing, stop placement and trailing move
variance around that mean; none of them changes its sign.
So every closed position now reports its result in R (net profit over money
actually at risk) and the running mean is tested against zero. Above the
configured minimum sample, if mean + sigma*SE < 0, new entries stop.
- SIGNIFICANTLY below, not merely below. A run of losers is ordinary variance
even for a profitable system; halting on the raw mean would be the same
act-on-noise error the MI gates exist to prevent. Using the standard error
means a wide spread simply demands more trades before the rule can fire.
- NET of swap and commission (ResolveClose already sums all three). Deliberate
and load-bearing: when the edge is zero, cost IS the expectancy, so a gross
version would measure a strategy nobody can trade.
- Reported in R so symbols, lot sizes and balances share one scale and one
mean. Trades without a stop are not scored rather than assigned a guessed R.
- LATCHED across restarts, like the daily halt and for the same reason: a
latch a reattach clears is not a latch. Clearing it means deleting the risk
state file, deliberately, after looking at why.
State is appended to the risk file length-guarded, so files written before this
still load and start their sample at zero rather than misreading.
Defaults 40 trades / 2 sigma; ExpectancyMinTrades = 0 disables it.
This does not make the strategy profitable and is not meant to. It stops paying
tuition on one the results say is losing, and does it on measurement rather than
on a drawdown limit finally being reached.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:20:00 -04:00
g_riskBudget . ConfigureExpectancy ( ExpectancyMinTrades , ExpectancySigma ) ;
2026-08-02 12:25:20 -04:00
g_riskBudget . Update ( ) ;
if ( EnableRiskGuard )
Print ( " Warrior EA: " , g_riskBudget . StatusLine ( ) ) ;
feat: expectancy stop - halt when the measured result says the strategy loses
The daily (4%) and total (8%) rules bound how FAST an account can lose. Nothing
noticed WHETHER it was losing. A negative-expectancy signal traded at 1% inside
that envelope breaches no rule and still arrives at zero - it just takes longer,
with every limit green the whole way down. That is the realistic way this EA
destroys an account, and no existing guard could see it.
THE ARITHMETIC THIS ENFORCES. Expected value per trade is p*TP - (1-p)*SL - cost.
With no directional edge p equals SL/(SL+TP), which is also the break-even rate,
so the payoff terms cancel exactly and EV = -cost. Expected P&L is -(trades) x
cost: strictly negative, proportional to activity. Measured here: directional
precision 23-24% against a 25% break-even, flat across every confidence tier,
with 58 points of spread on SP500. Sizing, stop placement and trailing move
variance around that mean; none of them changes its sign.
So every closed position now reports its result in R (net profit over money
actually at risk) and the running mean is tested against zero. Above the
configured minimum sample, if mean + sigma*SE < 0, new entries stop.
- SIGNIFICANTLY below, not merely below. A run of losers is ordinary variance
even for a profitable system; halting on the raw mean would be the same
act-on-noise error the MI gates exist to prevent. Using the standard error
means a wide spread simply demands more trades before the rule can fire.
- NET of swap and commission (ResolveClose already sums all three). Deliberate
and load-bearing: when the edge is zero, cost IS the expectancy, so a gross
version would measure a strategy nobody can trade.
- Reported in R so symbols, lot sizes and balances share one scale and one
mean. Trades without a stop are not scored rather than assigned a guessed R.
- LATCHED across restarts, like the daily halt and for the same reason: a
latch a reattach clears is not a latch. Clearing it means deleting the risk
state file, deliberately, after looking at why.
State is appended to the risk file length-guarded, so files written before this
still load and start their sample at zero rather than misreading.
Defaults 40 trades / 2 sigma; ExpectancyMinTrades = 0 disables it.
This does not make the strategy profitable and is not meant to. It stops paying
tuition on one the results say is losing, and does it on measurement rather than
on a drawdown limit finally being reached.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:20:00 -04:00
//--- Stated at init so the sample carried over from previous sessions is visible before any trade is
//--- placed, rather than only appearing in the line that halts trading.
if ( ExpectancyMinTrades > 0 & & g_riskBudget . ExpectancyTrades ( ) > 0 )
PrintFormat ( " Warrior EA: realised expectancy %.3f R over %d closed trades (halts below %.1f standard "
" errors under zero, after %d trades). Expected value per trade with no directional edge "
" is minus the cost, so a persistently negative figure here is the strategy, not variance. " ,
g_riskBudget . ExpectancyR ( ) , g_riskBudget . ExpectancyTrades ( ) , ExpectancySigma ,
ExpectancyMinTrades ) ;
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
}
//+------------------------------------------------------------------+
//| Block on alt-data and cross-asset reference-pair warm-up BEFORE |
//| any model is built below: both InitNeuralNetwork's fingerprint/ |
//| input-width pinning (alt-data) and a fresh model's first Build() |
//| (cross-asset) happen once, at construction, and never re-widen |
//| or re-include data/pairs that land later - see AltDataReload's |
//| and WarmBlocking's declaration comments for the OLD failures |
//| this avoids. Skipped for an unmapped symbol / in the tester or |
//| optimizer, same guard OnTimer's upkeep tick uses. |
//+------------------------------------------------------------------+
void WarmExternalData ( void )
{
2026-08-25 22:51:50 -04:00
if ( EnableAltData & & ! MQLInfoInteger ( MQL_TESTER ) & & ! MQLInfoInteger ( MQL_OPTIMIZATION ) & &
! MQLInfoInteger ( MQL_FORWARD ) )
2026-08-16 20:19:49 -04:00
{
g_lastAltDataRun = TimeCurrent ( ) ;
g_altDataFetch . Update ( _Symbol ) ;
}
2026-08-25 22:51:50 -04:00
if ( EnableCrossAsset & & ! MQLInfoInteger ( MQL_TESTER ) & & ! MQLInfoInteger ( MQL_OPTIMIZATION ) & &
! MQLInfoInteger ( MQL_FORWARD ) )
2026-08-16 21:08:41 -04:00
{
CCrossAssetPanel crossAssetWarmer ;
2026-08-22 00:25:52 -04:00
//--- A TIMEOUT HERE MUST BE AUDIBLE. Say so in the journal, because from that point on
//--- nothing else in the run ever mentions the missing pair again.
fix: the DB backfill could never run, and HEAD did not compile
Four defects in 64c5dd5/1a05e63, found by review + a baseline compile.
Goals 1-8 of that session are unchanged; this makes 6 and 8 actually reachable.
1. HEAD DID NOT COMPILE - 6 errors. CControlPanel::Minimize/Maximize were
declared `virtual bool ... override`, but CAppDialog declares both as
`virtual void` (Controls\Dialog.mqh). errors 265 + 404 on each, plus 151
on `bool ok = CAppDialog::Minimize()`. Return type is void now; there was
never a success flag to forward. Verified: 0 errors, 0 warnings.
2. THE BACKFILL COULD NEVER ADVANCE, and neither could the OOS continual
simulation (that one has been dead since it was written). Both are armed
at the instant convergence is declared, and both advance only from inside
Train(), one chunk per call. But ScheduleTrainingIfNeeded's only per-tick
ArmStudyEvent site sits in the `else` of a branch taken whenever
m_trainingComplete is set and m_trainRunActive is clear - which is exactly
the state FinalizeTrainRun() leaves behind one line before they are armed.
Train() was never called again, so the walks sat at their start index
forever: no "simulation complete" line, and not one row written to the DB
this feature exists to fill. Only a manual Resume/Retrain unstuck them.
Both flags now keep the model schedulable.
3. IN AI_HYBRID - the mode this ships in - the backfill was never even armed.
Ensemble members deploy at Train() ENTRY and return immediately (so no era
is wasted), which skips the era-end block the backfill was started from.
All four members were a no-op for a second, independent reason. Armed on
the ensemble deploy path too, from m_resumeBars/m_resumeOosCutoff.
4. RE-RUNS DUPLICATED ROWS. RegisterSignal inserts unconditionally - no key,
no duplicate check - and m_dbBackfillDone is in-memory, so every later
attach that retrained to convergence wrote a second full set of rows for
the same bars. The ranking would count one bar once per model that ever
deployed, weighting superseded opinions as heavily as the live one. A
.dbfill marker stamps the deployed era; written only on completion (an
interrupted walk redoes itself rather than ranking a partial window) and
deleted with the other sidecars on reset-weights.
Also: WarmBlocking's timeout was silent, which restored the exact silent
pin failure it was added to prevent - it now says so in the journal, and
returns true for "no reference pairs to wait for" so the warning stays rare
enough to be read.
Not addressed, needs a decision: the backfill scores the OOS window with the
checkpoint that was SELECTED as best on that same window, then writes those
win rates into the table filter weights rank on - the selection set consumed
twice, undiscounted, while the deploy gate right next to it applies a
family-wise correction for exactly that effect. The rows are also simulated
triple-barrier outcomes at today's spread sharing a table with realised
fills. The completion log line now states both plainly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:25:51 -04:00
if ( ! crossAssetWarmer . WarmBlocking ( ( ENUM_TIMEFRAMES ) Period ( ) , 4000 ) )
Print ( " Cross-asset warm-up did not finish syncing every reference pair within 4s - the model "
" built below will PIN whichever pairs are ready at that moment and never add the rest. "
" If the feature count looks short, detach and re-attach once the terminal has finished "
" downloading the reference symbols' history. " ) ;
2026-08-16 21:08:41 -04:00
}
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
}
//+------------------------------------------------------------------+
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
//+------------------------------------------------------------------+
//| IS THE SIGNAL DATABASE LIVE THIS RUN? The one answer, because |
//| OnInit asks it twice (open+journal, then the transaction-cycle |
//| check) and a run where those two disagreed would try to open a |
//| database it never initialised. |
//| |
//| Off in the tester/optimizer. A backtest opens the DB under |
//| FILE_COMMON, which EVERY parallel optimization agent opens too - |
//| one SQLite file, N writers, and the per-tick journal Update() |
//| behind them. Measured 2026-08-25: 12 agents, zero passes finished |
//| in 75 minutes. |
//| |
//| It also buys nothing, for a reason specific to this EA's current |
//| shape: the DB's only effect on a trading decision is |
//| ApplyPatternWeight overriding a filter's module weight, and that |
//| is declined for any self-ranking filter (CExpertSignalCustom's |
//| !filter.SelfRanked() guard). The AI members self-rank once their |
//| tiers are measured, and the classic votes that DID consume the |
//| ranking are gone - so a tester run's DB is written and never |
//| read. If a future filter consumes DB ranking WITHOUT self-ranking,|
//| revisit this: a backtest would then stop reproducing live. |
//+------------------------------------------------------------------+
bool SignalDatabaseActive ( void )
{
if ( ! UseDatabaseRanking )
return false ;
return ! ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION )
| | MQLInfoInteger ( MQL_FORWARD ) ) ;
}
//+------------------------------------------------------------------+
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
//| Open (or create) this config's fingerprinted DB, wire the trade |
//| journal to it, and run the meta-corpus staleness check - or, |
//| with ranking off, put the journal in tracking-only mode so the |
//| expectancy stop still gets fed. Returns false to fail OnInit. |
//+------------------------------------------------------------------+
bool InitDatabaseAndJournal ( const string caller )
{
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
//--- Deliberately NOT the whole journal: InitTrackingOnly below keeps close detection, MAE/MFE
//--- and the expectancy-stop feed alive, which a backtest genuinely has to simulate. Only the
//--- SQLite half is dropped - Update() already skips its INSERT when there is no DB.
if ( SignalDatabaseActive ( ) )
2026-07-14 22:36:27 -04:00
{
bool dbInitialized = false ;
string databaseFolderStructure [ ] = { eaName , " Databases " , " Signals " } ;
2026-07-22 22:51:04 -04:00
//--- fingerprinted so a topology/feature-set change that would produce a differently-shaped or
//--- differently-behaving model gets its own database, instead of silently mixing pattern-weight/
//--- trade-journal history from an incompatible prior config into the one now trading.
const string dbName = Symbol ( ) + " _ " + IntegerToString ( Period ( ) ) + " _ " + ComputeDbConfigFingerprint ( ) + " .db " ;
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
//--- 3.0: the pattern tables gained the netVote column and journaling went per-side (see
2026-08-22 00:25:52 -04:00
//--- CExpertSignalCustom::Direction()). 4.0 (2026-08-19): DB timestamps switched from GMT to
//--- BROKER time (user decision: one clock 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
const string dbVersion = " 4.0 " ;
2026-07-14 22:36:27 -04:00
PrintVerbose ( " Initializing Database... " ) ;
for ( int tries = 0 ; ! dbInitialized & & tries < 5 ; + + tries )
{
if ( ! dbm .Init ( dbVersion , databaseFolderStructure , dbName ) )
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Failed to initialize Database, retrying... " ) ;
2026-07-14 22:36:27 -04:00
RandomSleep ( ) ;
}
else
{
dbInitialized = true ;
break ;
}
}
if ( ! dbInitialized )
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Failed to initialize Database after retries " ) ;
return false ;
2026-07-14 22:36:27 -04:00
}
2026-07-22 22:51:04 -04:00
//--- Trade journal shares UseDatabaseRanking's DB connection/lifecycle rather than adding a
//--- second always-on toggle - see Database\TradeJournalManager.mqh's class comment.
feat(magic): assign the magic number once, then remember it
Expert_MagicNumber = 0 (the new default) means "draw one and write it down".
On first attach the EA picks a random magic in a distinctive band, persists it
to MQL5\Files\Warrior_<symbol>_<period>.magic, and reads that same value back on
every later start. Unique without anyone typing it, and STABLE.
Stability is the whole point. The magic is how the EA recognises its own
positions - a fresh one per start would leave every open position invisible to
the scheduled close-all, the risk-budget flatten and the journal's MAE/MFE walk:
trades still running that no code would ever manage again. So the value is
persisted before it is ever used to trade.
Stored TERMINAL-LOCAL rather than in Common\Files\Warrior_EA, on purpose: that
folder is the one wiped for a retrain, and positions outlive retrains. It also
gives two terminals on the same symbol different magics, which a chart-identity
hash could not.
Fallbacks, both of which stay stable without a file:
* tester/optimizer/forward use a magic derived from chart identity, so two
identical passes cannot differ.
* an unwritable file falls back to that same derived value, and says so.
Books occupy EVEN slots only, so one chart's short book (base+1) can never land
on another chart's long book.
WarriorOwnsMagic() now also recognises the legacy 2024/2025 pair permanently.
Without it, switching an existing chart to 0 while a position was open would
orphan that position. Every caller also matches the symbol, so claiming those
values can only reach positions on this EA's own chart.
Existing charts are untouched: MT5 stores inputs per chart, so the six live
charts keep the 2024 they already have and keep managing what they hold.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:35:58 -04:00
if ( ! journal .Init ( GetPointer ( dbm ) , WarriorBookMagic ( true ) ) )
2026-07-22 22:51:04 -04:00
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Failed to initialize trade journal table " ) ;
return false ;
2026-07-22 22:51:04 -04:00
}
2026-07-14 22:36:27 -04:00
}
fix: four risk-layer holes a funded account would eventually find
1. The expectancy stop was stone dead at shipped defaults. Its only feed -
RecordTradeResult inside CTradeJournalManager::Update() - ran solely under
UseDatabaseRanking, which ships false, so the da54639 halt was armed
(ExpectancyMinTrades=40) and never received a single closed trade. A risk
rule must not be a side effect of an analytics toggle: the journal gains
InitTrackingOnly(), Update() runs unconditionally from OnTick and skips
only the DB insert when no DB was initialized.
2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by
TCNormalizeVolume - correct for a user-entered fixed lot, but in the
risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/
step-0.01 symbols: double the intended risk, after CapRiskAmount already
clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario
the budget exists to close. CMoneyRiskBase now refuses the trade when the
risk-derived lot is below the broker minimum.
3. All trading was async fire-and-forget (SetAsyncMode(true)) with no
OnTradeTransaction handler and no retry: server retcodes were never
observed. Fail-safe for entries, not for closes - a silently rejected
close rode the position until the next bar (or next day for the timed
close window). Now synchronous, matching the risk-budget flatten's own
already-synchronous CTrade; on an H1 EA the latency is irrelevant.
4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no
OpenRiskAtStops) - pre-halt it could commit more than the remaining daily
allowance. A fixed lot cannot be scaled, so the rule is binary: its
loss-to-stop fits the remaining allowance whole or the trade is refused;
unpriceable risk (no SL) is refused while the budget is enabled.
Compile: 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00
else
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
//--- Reached when ranking is off OR this is a tester/optimizer run (see the guard above).
fix: four risk-layer holes a funded account would eventually find
1. The expectancy stop was stone dead at shipped defaults. Its only feed -
RecordTradeResult inside CTradeJournalManager::Update() - ran solely under
UseDatabaseRanking, which ships false, so the da54639 halt was armed
(ExpectancyMinTrades=40) and never received a single closed trade. A risk
rule must not be a side effect of an analytics toggle: the journal gains
InitTrackingOnly(), Update() runs unconditionally from OnTick and skips
only the DB insert when no DB was initialized.
2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by
TCNormalizeVolume - correct for a user-entered fixed lot, but in the
risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/
step-0.01 symbols: double the intended risk, after CapRiskAmount already
clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario
the budget exists to close. CMoneyRiskBase now refuses the trade when the
risk-derived lot is below the broker minimum.
3. All trading was async fire-and-forget (SetAsyncMode(true)) with no
OnTradeTransaction handler and no retry: server retcodes were never
observed. Fail-safe for entries, not for closes - a silently rejected
close rode the position until the next bar (or next day for the timed
close window). Now synchronous, matching the risk-budget flatten's own
already-synchronous CTrade; on an H1 EA the latency is irrelevant.
4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no
OpenRiskAtStops) - pre-halt it could commit more than the remaining daily
allowance. A fixed lot cannot be scaled, so the rule is binary: its
loss-to-stop fits the remaining allowance whole or the trade is refused;
unpriceable risk (no SL) is refused while the budget is enabled.
Compile: 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00
//--- No DB, but the close-detection path still runs: it feeds the expectancy stop
//--- (g_riskBudget.RecordTradeResult). Until 2026-08-11 that feed existed only under
//--- UseDatabaseRanking (ships false), so the expectancy halt could never arm on a default
//--- install - see InitTrackingOnly's comment in Database\TradeJournalManager.mqh.
feat(magic): assign the magic number once, then remember it
Expert_MagicNumber = 0 (the new default) means "draw one and write it down".
On first attach the EA picks a random magic in a distinctive band, persists it
to MQL5\Files\Warrior_<symbol>_<period>.magic, and reads that same value back on
every later start. Unique without anyone typing it, and STABLE.
Stability is the whole point. The magic is how the EA recognises its own
positions - a fresh one per start would leave every open position invisible to
the scheduled close-all, the risk-budget flatten and the journal's MAE/MFE walk:
trades still running that no code would ever manage again. So the value is
persisted before it is ever used to trade.
Stored TERMINAL-LOCAL rather than in Common\Files\Warrior_EA, on purpose: that
folder is the one wiped for a retrain, and positions outlive retrains. It also
gives two terminals on the same symbol different magics, which a chart-identity
hash could not.
Fallbacks, both of which stay stable without a file:
* tester/optimizer/forward use a magic derived from chart identity, so two
identical passes cannot differ.
* an unwritable file falls back to that same derived value, and says so.
Books occupy EVEN slots only, so one chart's short book (base+1) can never land
on another chart's long book.
WarriorOwnsMagic() now also recognises the legacy 2024/2025 pair permanently.
Without it, switching an existing chart to 0 while a position was open would
orphan that position. Every caller also matches the symbol, so claiming those
values can only reach positions on this EA's own chart.
Existing charts are untouched: MT5 stores inputs per chart, so the six live
charts keep the 2024 they already have and keep managing what they hold.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:35:58 -04:00
journal . InitTrackingOnly ( WarriorBookMagic ( true ) ) ;
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
return true ;
}
2026-07-16 00:56:33 -04:00
//+------------------------------------------------------------------+
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
//| Create every signal instance this run's Use_*/Enable* inputs ask |
//| for (AI architectures + the meta head + the classic votes + the |
//| filters), wire the meta gate onto the root signal, apply each |
//| classic vote's tuned periods, apply the shared AI configuration, |
//| and register every filter on the root signal EXACTLY ONCE (see |
//| the comment above the filter-registration block for why it must |
//| not live inside a retry loop). Returns false to fail OnInit. |
2026-07-16 00:56:33 -04:00
//+------------------------------------------------------------------+
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
bool CreateAndConfigureSignals ( CExpertSignalCustom * signal , const int maxRetryOnError , const string caller )
{
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
//--- Re-derived from the per-NN inputs (they are also initialized at file scope in
2026-08-22 00:25:52 -04:00
//--- Variables\Variables.mqh; this re-assignment is the init-order-safe truth).
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
EnablePAI = Use_MLP ;
EnableCONV = Use_CONV ;
EnableLSTM = Use_LSTM ;
EnableHYBRID = Use_CONVLSTM ;
2026-07-14 22:36:27 -04:00
// Creating instances of signals
CSignalPAI * PAI = CreateSignalWithRetry < CSignalPAI > ( maxRetryOnError , EnablePAI ) ;
CSignalCONV * CONV = CreateSignalWithRetry < CSignalCONV > ( maxRetryOnError , EnableCONV ) ;
CSignalLSTM * LSTM = CreateSignalWithRetry < CSignalLSTM > ( maxRetryOnError , EnableLSTM ) ;
2026-07-27 22:08:55 -04:00
CSignalHYBRID * HYBRID = CreateSignalWithRetry < CSignalHYBRID > ( maxRetryOnError , EnableHYBRID ) ;
2026-07-14 22:36:27 -04:00
//--- register whichever AI signal instances this run created, so the control panel can drive
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
//--- training/weight actions on exactly this run's current config (never another config's files)
2026-07-14 22:36:27 -04:00
g_aiSignalCount = 0 ;
if ( EnablePAI & & PAI ! = NULL )
RegisterAISignal ( PAI ) ;
if ( EnableCONV & & CONV ! = NULL )
RegisterAISignal ( CONV ) ;
if ( EnableLSTM & & LSTM ! = NULL )
RegisterAISignal ( LSTM ) ;
2026-07-27 22:08:55 -04:00
if ( EnableHYBRID & & HYBRID ! = NULL )
RegisterAISignal ( HYBRID ) ;
2026-07-14 22:36:27 -04:00
CSignalNewsFilter * newsFilter = CreateSignalWithRetry < CSignalNewsFilter > ( maxRetryOnError , EnableNewsFilter ) ;
CSignalSessionFilter * sessionFilter = CreateSignalWithRetry < CSignalSessionFilter > ( maxRetryOnError , EnableSessionFilter ) ;
refactor(inputs): 96 -> 70 inputs; remove two untested/unusable filter modules
Every removal below is FINGERPRINT-NEUTRAL by construction: each retired
input is pinned to the exact value it already shipped with, so running
models keep their filenames and resume rather than restarting at era 0.
Verified field by field against BuildConfigFingerprint.
Removed as inputs, kept as pinned constants (the value was never a
preference the user had a basis to change):
- OutputNeuronsCount. The regression head predicts a continuous quantity
the triple-barrier label does not contain; the target is an EVENT, so
the right output is its probability. The regression code paths stay
implemented and dormant - they cost nothing and removing them would
touch every scoring path at once.
- MinRecall. A safety floor, not a preference, and the only direction a
user can move it is the harmful one: raising it past what the config
reaches yields NO model, not a better one (observed repeatedly at 60).
- SwingConfirmationBars. Stopped gating the labels with the relabel, but
is STILL load-bearing for the swing-context input features - it is the
ZigZag repainting embargo, and without it those 9 features read a leg
the live bar could not have had yet. Pinned, not deleted.
- MaxErasPerRun (runaway backstop, never reached in a healthy run),
FreezePriorCalibration (unanswerable by a user; near-balanced labels
make the priors stable anyway), VerboseMode (developer view, joins
DebuggingMode), MACD/Ichimoku periods x6 (both indicators ship
disabled, and as optimizer dimensions they are pure overfitting
surface - the AI auto-tuner is the supported way to move them).
- SignalClusterWindow -> 3, no longer an input. Barrier labels make
consecutive setups real, which argued for 0; it is not 0 because on D1+
a 6-bar window spans over a week and two arrows a day apart on a
weekly-scale move are one event. 3 splits it correctly by timeframe.
- EnableOnlineLearning -> ON. Adapting to a changing market is what keeps
a months-attached model from going stale, and the rolling-accuracy
freeze is what makes it safe. See the caveat noted in the handoff: it
had not been forward-tested on a live feed when this became default.
Removed entirely:
- Intraday Time Filter (5 inputs + Signals/SignalITF.mqh). Two of its
five inputs were raw BITMASKS, which is an implementation detail
exposed as a control. The job is covered three times over by things
that are declarative or that learn: the session filter, the
time-of-day/day-of-week input features (the network discovers which
hours are good rather than being told), and the journal's time buckets.
- Market Depth Filter (5 inputs + Signals/SignalMarketDepth.mqh, plus
its OnInit probe and OnDeinit release). It needs real level-2 data
that this broker - and most retail MT5 brokers - do not provide, so
the module has never once executed against real data. Shipping four
tuning dropdowns for an untested path is worse than shipping nothing:
the only users who could enable it would be its first-ever testers,
live. If DOM returns it should be a FEATURE fed to the network, not a
rule-based veto with hand-tuned thresholds - imbalance is data.
- IndicatorTuneTrials, replaced by ComputeTuneTrialBudget(). The useful
budget depends on how many parameters are actually being searched,
which depends on which features are enabled - so one number meant
wildly different things run to run. The shipped 32 was ~10 candidates
per dimension against one enabled indicator (wasteful: each costs
GA_SEEDS full training runs) and under one per dimension against all
nine (blind). Now population ~ 4 x active dimensions, clamped [8,64],
with CADIndicatorTuner::ActiveDimensions() defined immediately above
PerturbRandom() so the two cannot drift apart.
- Six orphaned enums (TUNE_TRIALS_PRESET, DOM_*, ENTRY_HOUR_OF_DAY,
TIME_FILTER_DAY_OF_WEEK), 81 lines.
Other UX:
- SL_ATR_x1 / TP_ATR_x3 now carry the "(classic)" default marker every
other preset enum in the file already used. Nothing in the SL/TP
dropdowns previously told a user which pair was the shipped default -
which matters far more since the relabel, because those two define the
labels and changing either forces a retrain.
- Neural Network section moved directly ABOVE AI Input Features: choose
the architecture, then choose what it sees. NN Optimizer / Performance
stays last - the Adam/Sgd inputs are declared in AI/Network.mqh and
render immediately after that divider.
- News feature + window moved to the end of the AI feature list, below
Wyckoff Bar Inversion.
- Dropped "(0-100)" from Min vote to open - it is an enum, not a number.
Both builds compile 0 errors / 0 warnings. No retrain forced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:22:02 -04:00
//--- CSignalITF and CSignalMarketDepth were removed 2026-08-01 - see the removal notes in
//--- Variables\Inputs.mqh (bitmask-configured time filter, and an untestable DOM module).
2026-07-18 17:29:38 -04:00
CSignalRiskGuard * riskGuard = CreateSignalWithRetry < CSignalRiskGuard > ( maxRetryOnError , EnableRiskGuard ) ;
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
if ( ( EnablePAI & & PAI = = NULL ) | | ( EnableCONV & & CONV = = NULL ) | | ( EnableLSTM & & LSTM = = NULL ) | | ( EnableHYBRID & & HYBRID = = NULL ) | | ( EnableNewsFilter & & newsFilter = = NULL ) | | ( EnableSessionFilter & & sessionFilter = = NULL ) | | ( EnableRiskGuard & & riskGuard = = NULL ) )
2026-07-14 22:36:27 -04:00
{
Print ( " Critical signal initialization failed, cannot proceed " ) ;
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
return false ;
2026-07-14 22:36:27 -04:00
}
// Set filter parameters
2026-08-22 00:25:52 -04:00
//--- CSignalRiskGuard takes no parameters any more: the thresholds, the anchors and the state file
//--- all moved to g_riskBudget (configured in OnInit, evaluated per tick).
2026-07-14 22:36:27 -04:00
if ( EnableSessionFilter )
{
sessionFilter . TradeLondonSession ( SF_trade_LondonSession ) ;
sessionFilter . TradeNewYorkSession ( SF_trade_NewYorkSession ) ;
sessionFilter . TradeTokyoSession ( SF_trade_TokyoSession ) ;
}
if ( EnableNewsFilter )
{
newsFilter . SetMinImpact ( NF_MinImpact ) ;
newsFilter . SetLookbackMinutes ( NF_LookMinutes ) ;
}
2026-07-27 22:18:50 -04:00
if ( EnablePAI )
2026-08-01 11:27:28 -04:00
ConfigureAISignal ( PAI ) ;
if ( EnableCONV )
ConfigureAISignal ( CONV ) ;
if ( EnableLSTM )
ConfigureAISignal ( LSTM ) ;
2026-07-27 22:18:50 -04:00
if ( EnableHYBRID )
2026-08-01 11:27:28 -04:00
ConfigureAISignal ( HYBRID ) ;
2026-07-14 22:36:27 -04:00
// Add filters
PrintVerbose ( " Initializing Signal filters... " ) ;
//--- added exactly once, before the DB retry loop below - these calls don't depend on DB success at
//--- all (every pointer here was already validated non-NULL above), but living inside the loop body
//--- meant a DB open/transaction failure that triggered a retry would re-run AddFilterToSignal() and
//--- register the same filter pointer a second time in signal's CArrayObj; since that array frees its
//--- elements on destruction, a duplicate entry means the same pointer gets deleted twice on shutdown
//--- (heap corruption), which could easily explain instability across a later remove/re-add cycle.
bool filtersAdded = true ;
filtersAdded & = ( EnableSessionFilter ? AddFilterToSignal ( signal , sessionFilter ) : true ) ;
filtersAdded & = ( EnableNewsFilter ? AddFilterToSignal ( signal , newsFilter ) : true ) ;
2026-07-18 17:29:38 -04:00
filtersAdded & = ( EnableRiskGuard ? AddFilterToSignal ( signal , riskGuard ) : true ) ;
2026-07-14 22:36:27 -04:00
filtersAdded & = ( EnablePAI ? AddFilterToSignal ( signal , PAI ) : true ) ;
filtersAdded & = ( EnableCONV ? AddFilterToSignal ( signal , CONV ) : true ) ;
filtersAdded & = ( EnableLSTM ? AddFilterToSignal ( signal , LSTM ) : true ) ;
2026-07-27 22:08:55 -04:00
filtersAdded & = ( EnableHYBRID ? AddFilterToSignal ( signal , HYBRID ) : true ) ;
2026-07-14 22:36:27 -04:00
if ( ! filtersAdded )
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Error loading filters " ) ;
return false ;
2026-07-14 22:36:27 -04:00
}
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
return true ;
}
//+------------------------------------------------------------------+
//| Confirm the DB open/begin-transaction/commit/close cycle works, |
//| retrying up to maxRetryOnError times. Deliberately does NOT |
//| touch signal/filter state - CreateAndConfigureSignals() already |
//| registered every filter exactly once, so a retry here can never |
//| re-run that registration. Returns false to fail OnInit. |
//+------------------------------------------------------------------+
bool VerifyDatabaseTransactionCycle ( const string caller , const int maxRetryOnError )
{
2026-07-14 22:36:27 -04:00
bool filterSuccess = false ;
for ( int tries = 0 ; tries < maxRetryOnError ; + + tries )
{
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
//--- SignalDatabaseActive(), not UseDatabaseRanking: in the tester InitDatabaseAndJournal
//--- never called dbm.Init(), so opening here would fail and burn every retry.
if ( SignalDatabaseActive ( ) & & ! dbm . OpenDatabase ( ) )
2026-07-14 22:36:27 -04:00
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Error opening database, retrying... " ) ;
2026-07-14 22:36:27 -04:00
RandomSleep ( ) ;
continue ;
}
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
if ( SignalDatabaseActive ( ) & & ! dbm . BeginTransaction ( ) )
2026-07-14 22:36:27 -04:00
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Error starting transaction, retrying... " ) ;
2026-07-14 22:36:27 -04:00
dbm . CloseDatabase ( ) ; // Ensure the database is closed before retry
RandomSleep ( ) ;
continue ;
}
perf(tester): skip the signal DB in tester/optimizer, drop ExportFeaturesOnly
Two removals of work that a backtest was paying for and never using.
1. SignalDatabaseActive() gates the signal DB off in tester/optimizer.
A backtest opened the fingerprinted SQLite DB under FILE_COMMON - and so
did every parallel optimization agent, against the same file, with the
per-tick journal Update() behind them. Measured 2026-08-25 on a 12-agent
SP500 H4 run: zero passes completed in 75 minutes.
It bought nothing, for a reason specific to this EA's current shape: the
DB's only effect on a trading decision is ApplyPatternWeight overriding a
filter's module weight, and that is declined for any self-ranking filter
(CExpertSignalCustom's !filter.SelfRanked() guard). The AI members
self-rank once their tiers are measured, and the classic votes that DID
consume the ranking are gone - so a tester run's DB was written and never
read. Skipping it changes no decision.
One predicate, not two inline guards: OnInit asks the question twice
(InitDatabaseAndJournal, then VerifyDatabaseTransactionCycle) and a run
where those disagreed would try to open a database it never initialised.
The tester now takes journal.InitTrackingOnly(), so close detection,
MAE/MFE and the expectancy-stop feed still run - only the SQLite half is
dropped, and Update() already skipped its INSERT when there is no DB.
Caveat recorded at the predicate: if a future filter consumes DB ranking
WITHOUT self-ranking, this needs revisiting - a backtest would then stop
reproducing live.
2. ExportFeaturesOnly and its two exporters are gone.
Research-only CSV dumps (feature matrix + a hardcoded 8-symbol x 5-TF raw
rates grid), superseded by the research/ python path that reads its own
data. Removed the input, m_exportFeaturesOnly, the setter, both method
declarations, ExportFeatureMatrix()/ExportRawRates() (111 lines in
AutoTune.mqh), the OnTick early-return, and the ctor initialiser.
The config-lock bypass it owned collapses to the plain tester test:
`if(!inTesterOrOpt && !AcquireConfigLock())`. Shared helpers it called -
ServableBars, EnsureBarCachesCapacity, ResizeBuffers, RefreshData - all
have other callers and are untouched.
Compile-verified in _claude_stage: 0 errors, 0 warnings, identical to the
baseline taken before either edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:32:44 -04:00
if ( SignalDatabaseActive ( ) & & ( ! dbm . CommitTransaction ( ) | | ! dbm . CloseDatabase ( ) ) )
2026-07-14 22:36:27 -04:00
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Error committing transaction or closing database, retrying... " ) ;
2026-07-14 22:36:27 -04:00
RandomSleep ( ) ;
continue ;
}
filterSuccess = true ;
break ; // Success if all operations complete without error
}
if ( ! filterSuccess )
{
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
Print ( caller + " : Failed after all retries " ) ;
return false ; // Return failure if retries are exhausted
2026-07-14 22:36:27 -04:00
}
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
return true ;
}
//+------------------------------------------------------------------+
//| Create the control panel and do the one-time chart-object |
//| cleanup/event-flag setup that only matters once the panel (or |
//| its absence) is settled. |
//+------------------------------------------------------------------+
void FinalizeStartupUI ( const string caller )
{
if ( ! CreateControlPanel ( ) )
Print ( caller + " : WARNING - control panel failed to initialize; trading/training continue normally, "
" but no GUI panel will be available for this run " ) ;
//--- one-time cleanup: an earlier build used Comment() plus a separate background rectangle object
//--- that turned out to render ON TOP of the text (Comment() has no built-in background/styling
//--- parameters at all) - delete any leftover from a prior run now that status text is a single
//--- self-contained OBJ_LABEL (see SetStatusLabel()) with its own BGCOLOR fill instead
if ( ObjectFind ( 0 , " WarriorCommentBG " ) > = 0 )
ObjectDelete ( 0 , " WarriorCommentBG " ) ;
//--- required for CAppDialog's caption-bar drag to work at all - without it, the chart never delivers
//--- CHARTEVENT_MOUSE_MOVE and the panel silently ignores drag attempts
ChartSetInteger ( 0 , CHART_EVENT_MOUSE_MOVE , true ) ;
}
//+------------------------------------------------------------------+
//| IMPORTANT: no failure branch below (nor in any helper it calls - |
//| AddFilterToSignal(), InitializeSignal(), InitializeTrailing(), |
//| InitializeMoneyManagement()) may call Expert.Deinit() before |
//| returning INIT_FAILED/false. |
//+------------------------------------------------------------------+
int OnInit ( )
{
//--- FIRST: adopt the chart's tuned indicator periods (if a gated auto-tune install ever wrote them).
//--- Must precede ComputeDbConfigFingerprint() and the classic-signal configuration below, both of
//--- which consume the g_Tuned* values - see Variables\TunedPeriods.mqh for the whole contract.
LoadTunedPeriods ( ) ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
//--- Arm the tester pass self-profile - see its globals above OnTick().
g_tpActive = ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) ) ;
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
//--- clears out whatever status label text was left over from before this OnInit() ran (stale text
//--- from a prior "warm" re-init - e.g. an input-parameter change, which reuses this same running
//--- instance rather than a fresh one - would otherwise sit unchanged and look like nothing is
//--- happening) so it's obvious the moment training/signal init actually resumes producing new status text
//--- START FROM A GUARANTEED-CLEAN CHART. Before anything is drawn, sweep every object namespace this
//--- EA owns (WarriorChartPrefixes). Chart objects live in the chart PROFILE, not in the EA, so they
//--- outlive the process: a deinit that ran out of MetaTrader's ~4,500 ms budget, a crash, a terminal
//--- kill, or an .ex5 replaced while attached all leave objects behind that no later deinit will ever
//--- own. Deleting the EA's files does not remove them either, which is why they read as corruption.
//--- Arrows are INCLUDED in this sweep: LoadChartSignals restores them from their sidecar moments later
//--- and already opens with its own arrow sweep, so purging here costs nothing and removes any orphan
//--- that the sidecar does not account for - the ones that would otherwise be adopted by the next model
//--- to attach, because SaveChartSignals rebuilds that sidecar by SCANNING the chart.
//--- Runs BEFORE SetStatusLabel below, or it would delete the label it just created.
PurgeStaleChartObjectsAndReport ( __FUNCTION__ ) ;
SetStatusLabel ( " Warrior EA: initializing... " ) ;
//--- WHICH BINARY IS ACTUALLY RUNNING. Read it FIRST when a fix appears not to have taken.
PrintFormat ( " %s: build tag %s | COMPILED %s " , __FUNCTION__ , WARRIOR_BUILD_TAG ,
TimeToString ( __DATETIME__ , TIME_DATE | TIME_MINUTES ) ) ;
PrintFormat ( " %s: trade settings snapshot - NNs=%s Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d TrailingStrategy=%d MM_STRATEGY=%d " ,
__FUNCTION__ , EnabledNNSummary ( ) , ( int ) Entry_Multiplier , ( int ) SL_Mode , ( int ) TP_Mode ,
( int ) TrailingStrategy , ( int ) MM_STRATEGY ) ;
refactor(trade-mgmt): remove all confidence-scaled trade management
Five modes went, all of them staking real risk on the model's confidence:
Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target
(TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size
(CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source
input and the CONFIDENCE_SOURCE enum, whose only job was choosing which
number those five read.
The reason is calibration, not correctness: the confidence magnitude is
known to be miscalibrated against the label prior, so every one of these
modes multiplied money by a quantity whose units were never established.
The DB arm had a second, independent defect - since the tester DB guard
(SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero
live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live
trading. And what the DB produces is a filter-RANKING win rate, not a
per-trade win probability.
Both confidence numbers are still recorded per trade (aiConfidence /
dbConfidence) and still bucketed against outcome in TradeJournalReport.
Recording is what keeps the question answerable; acting on it was the
part with no evidence behind it. ConfidenceBridge.mqh now carries an
explicit telemetry-only rule at the top.
ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at
once and MT5 does not validate an enum input replayed from a saved .set
or a stored optimization pass. TRAILING_STRATEGY and
MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep
the numbers they were saved as, and ValidateBarrierInputs is widened into
ValidateTradeManagementInputs covering SL_Mode, TP_Mode,
Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a
chart saved with the Intelligent stop would feed SL_Mode = -1 into a
multiplier now used verbatim, placing the stop on the wrong side of entry.
RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in
BuildModelFingerprint() or ComputeDbConfigFingerprint() since the
swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db
re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the
CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence().
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//--- HARD GATE on every trade-management enum. This is not hypothetical: MT5 replays a saved .set
//--- (or a stored optimization pass) without validating enum members, so an option removed between
//--- builds keeps being fed back in. Five such options were removed 2026-08-25 across four of these
//--- enums, and the stalest of them (SL_Mode = -1) would now place a stop on the wrong side of entry.
if ( ! ValidateTradeManagementInputs ( ) )
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
return INIT_FAILED ;
if ( ! ValidateRiskInputs ( ) )
return INIT_FAILED ;
ConfigureRiskBudget ( ) ;
LoadSignalsVisibilityState ( ) ;
int maxRetryOnError = 5 ;
string functionName = __FUNCTION__ ;
// Initialize random seed based on the number of milliseconds since the system started
//--- One process-wide seeding at startup; every fresh topology re-seeds with its own model id
//--- on top of this (System\Random.mqh).
WarriorRandSeed ( " OnInit " ) ;
// Initialize expert
if ( ! RetryInitStep ( StepExpertInit , " initialize expert " , maxRetryOnError , functionName ) )
return INIT_FAILED ;
Expert . OnChartEventProcess ( true ) ;
//--- Alt-data and cross-asset warm-up MUST be on disk/synced BEFORE any model is built below - see
//--- WarmExternalData()'s declaration comment for why both are blocking, not left to the timer.
WarmExternalData ( ) ;
// Creating signal
PrintVerbose ( " Initializing Signal... " ) ;
CExpertSignalCustom * signal = CreateSignalWithRetry < CExpertSignalCustom > ( maxRetryOnError , true ) ;
if ( signal = = NULL )
return INIT_FAILED ;
InitializeSignal ( signal ) ;
// Initializing Database
if ( ! InitDatabaseAndJournal ( functionName ) )
return INIT_FAILED ;
//+------------------------------------------------------------------+
//| The per-NN inputs (Use_MLP/Use_CONV/Use_LSTM/Use_CONVLSTM) pick |
//| which direction architectures this run trades/trains - any |
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
//| subset, each an independent model. |
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
//+------------------------------------------------------------------+
if ( ! CreateAndConfigureSignals ( signal , maxRetryOnError , functionName ) )
return INIT_FAILED ;
//--- DB open/begin/commit/close cycle, retried - deliberately AFTER filter registration above (see
//--- CreateAndConfigureSignals()'s own comment on why filters must not sit inside this retry loop).
if ( ! VerifyDatabaseTransactionCycle ( functionName , maxRetryOnError ) )
return INIT_FAILED ;
2026-07-14 22:36:27 -04:00
// Trailing logic
PrintVerbose ( " Initializing Trailing... " ) ;
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
if ( ! RetryInitStep ( StepInitTrailing , " initialize Trailing " , maxRetryOnError , functionName ) )
2026-07-14 22:36:27 -04:00
return INIT_FAILED ;
// Creation of money object
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
if ( ! RetryInitStep ( StepInitMoneyManagement , " initialize Money Management " , maxRetryOnError , functionName ) )
2026-07-14 22:36:27 -04:00
return INIT_FAILED ;
// Check all trading objects parameters
PrintVerbose ( " Validating settings... " ) ;
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
if ( ! RetryInitStep ( StepValidateSettings , " validate settings " , maxRetryOnError , functionName ) )
2026-07-14 22:36:27 -04:00
return INIT_FAILED ;
// Tuning of all necessary indicators
PrintVerbose ( " Initializing Indicators... " ) ;
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
if ( ! RetryInitStep ( StepInitIndicators , " initialize Indicators " , maxRetryOnError , functionName ) )
2026-07-14 22:36:27 -04:00
return INIT_FAILED ;
2026-08-22 00:25:52 -04:00
//--- setting timer: always on (short interval) so control-panel upkeep and other periodic checks
//--- run on a fixed schedule regardless of tick activity - a quiet/after-hours symbol can go long
//--- stretches without a single OnTick() call, and self-healing logic that only lives in OnTick()
//--- would never run during that stretch.
2026-08-25 22:51:50 -04:00
//--- NEVER in the tester/optimizer/forward pass: TimeCurrent() there is SIMULATED, so the manual
//--- throttle at DB_RANKING_INTERVAL_SECONDS (below, paced off the same simulated clock) never
//--- actually elapses - this ran on essentially every one of the belt's ~2,600 fires per pass
//--- until this guard, opening the DB and running ProcessBufferedSignals()/UpdateSignalsWeights()
//--- every time. A tester pass is inference-only (m_inferenceOnly); DB-derived filter weights are
//--- a live-learning feature with nothing to update on a pass that never trains.
if ( UseDatabaseRanking & & ! MQLInfoInteger ( MQL_TESTER ) & & ! MQLInfoInteger ( MQL_OPTIMIZATION ) & &
! MQLInfoInteger ( MQL_FORWARD ) )
2026-07-14 22:36:27 -04:00
Expert . OnTimerProcess ( true ) ;
refactor(dry): one retry-and-report for OnInit's five init steps
Trailing, money management, settings validation, indicator setup and
timer registration each carried their own copy of the same 18-line
retry loop - a bool, a counted loop, a RandomSleep backoff, and two
Print lines - differing only in which call they made and what they
called it. Five places for the retry count, the backoff and the failure
wording to drift apart, and OnInit was 635 lines partly because of it.
RetryInitStep() is now the only copy. `what` completes both sentences
the loop printed, so the journal reads exactly as it did; `caller` is
passed in rather than read from __FUNCTION__ so the line still names
OnInit and not the helper.
The five steps become one-line wrappers because MQL5 function pointers
bind neither a method call nor an argument, and these are two of each
(Expert.ValidationSettings/InitIndicators, and the timer's interval).
That interval moves to WARRIOR_TIMER_INTERVAL_MS beside its wrapper,
taking its full rationale with it instead of leaving it stranded in the
middle of OnInit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:53:12 -04:00
//--- Interval and the reasoning behind it: WARRIOR_TIMER_INTERVAL_MS, beside StepSetTimer().
if ( ! RetryInitStep ( StepSetTimer , " set the timer " , maxRetryOnError , functionName ) )
2026-07-14 22:36:27 -04:00
return INIT_FAILED ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- THE COMBINED-VOTE ARROW LAYER. Bound here, at the end of init, because the key depends on the
//--- config fingerprint and the thresholds depend on the validated inputs - both settled by now.
//--- Keyed on symbol + period + config fingerprint rather than on any one model's filename: the vote
//--- belongs to the CHART, and on an ensemble no single member owns it.
2026-08-26 12:09:36 -04:00
//--- THE PINNED THRESHOLD IF THERE IS ONE, and the seed only before the first era has been scored.
//--- An arrow is a claim about a threshold, and the store DISCARDS its cache when that threshold
//--- moves - so handing it the Signal_ThresholdOpen seed while the chart trades a derived 15% both
//--- threw away every restorable arrow AND labelled the survivors with a threshold the EA does not
//--- use. LoadModelStats() has already restored g_ensDerivedThreshold by this point in init (its
//--- "restored the ensemble record" line prints before this one), which is what makes reading it here
//--- safe. Display-only: the trade path gets the same number from PublishVoteThreshold().
double arrowThreshold = ( g_ensDerivedThreshold > 0.0 ) ? g_ensDerivedThreshold
: ( double ) Signal_ThresholdOpen ;
fix(chart): stale combined-vote arrows survived every wipe, because two files lived outside Warrior_EA\
Operator report: arrows labelled as restored from a previous session on a
fleet training from era 0. Confirmed - all six charts restored 115-431
combined-vote arrows drawn by models that no longer exist.
TWO INDEPENDENT DEFECTS, either of which alone causes it.
1. CVoteArrowStore::Discard() HAD NO CALLER.
The member-scoped .arrows file is cleared by ClearPersistedChartSignals on a
fresh topology. The CHART-scoped .votearrows store has an equivalent
Discard(), written for exactly this, and nothing ever called it. The store
is keyed on the DB config fingerprint, which does not move when a model is
wiped, so it reloaded across any reset - fresh topology, panel weight reset,
or a model-file wipe.
A vote is a claim made by a specific set of members. If any member rebuilt
from scratch this run, the whole stored history is void, so
g_warriorFreshTopologyThisRun is now raised wherever a member discards
weights or builds a fresh topology, and the store Discards instead of Loads.
2. TWO WARRIOR FILES LIVED OUTSIDE Warrior_EA\.
.sigvis and .votearrows were written to the ROOT of Common\Files, outside
the one directory that "wipe the Warrior EA files" has always meant. Two
consecutive wipes this session left them standing untouched, and neither
wipe was as fresh as reported. Both now live under Warrior_EA\ChartState\.
A wipe that does not remove all of a program's state is not a wipe, and
nothing in the log told the operator which files were missed.
NOTE for anyone re-running the wipe: pre-existing WarriorVote_*.votearrows and
Warrior_EA_*.sigvis in the Common\Files ROOT are orphaned by this change and
should be deleted once.
Build tag -> fleet-pool-v3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 21:07:52 -04:00
g_voteArrows . Configure ( WARRIOR_STATE_DIR + " ChartState \\ " + StringFormat ( " WarriorVote_%s_%d_%s " , _Symbol , ( int ) _Period , ComputeDbConfigFingerprint ( ) ) ,
2026-08-26 12:09:36 -04:00
arrowThreshold , ( double ) VOTE_EXIT_DISABLED_THRESHOLD ,
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- Inactive in the raw view (the per-model layer owns the chart there) and
//--- in the tester, whose charts are throwaway.
! DrawUnfilteredSignals & & ! ( MQLInfoInteger ( MQL_TESTER ) | |
MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) ) ) ;
fix(chart): stale combined-vote arrows survived every wipe, because two files lived outside Warrior_EA\
Operator report: arrows labelled as restored from a previous session on a
fleet training from era 0. Confirmed - all six charts restored 115-431
combined-vote arrows drawn by models that no longer exist.
TWO INDEPENDENT DEFECTS, either of which alone causes it.
1. CVoteArrowStore::Discard() HAD NO CALLER.
The member-scoped .arrows file is cleared by ClearPersistedChartSignals on a
fresh topology. The CHART-scoped .votearrows store has an equivalent
Discard(), written for exactly this, and nothing ever called it. The store
is keyed on the DB config fingerprint, which does not move when a model is
wiped, so it reloaded across any reset - fresh topology, panel weight reset,
or a model-file wipe.
A vote is a claim made by a specific set of members. If any member rebuilt
from scratch this run, the whole stored history is void, so
g_warriorFreshTopologyThisRun is now raised wherever a member discards
weights or builds a fresh topology, and the store Discards instead of Loads.
2. TWO WARRIOR FILES LIVED OUTSIDE Warrior_EA\.
.sigvis and .votearrows were written to the ROOT of Common\Files, outside
the one directory that "wipe the Warrior EA files" has always meant. Two
consecutive wipes this session left them standing untouched, and neither
wipe was as fresh as reported. Both now live under Warrior_EA\ChartState\.
A wipe that does not remove all of a program's state is not a wipe, and
nothing in the log told the operator which files were missed.
NOTE for anyone re-running the wipe: pre-existing WarriorVote_*.votearrows and
Warrior_EA_*.sigvis in the Common\Files ROOT are orphaned by this change and
should be deleted once.
Build tag -> fleet-pool-v3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 21:07:52 -04:00
//--- A VOTE IS A CLAIM MADE BY A SPECIFIC SET OF MEMBERS. If any member rebuilt from scratch this
//--- run, every stored arrow was drawn by a model that no longer exists - restoring them shows a
//--- freshly-initialised ensemble's chart covered in a dead ensemble's calls, and the next
//--- snapshot writes them straight back out. Discard, do not Load.
if ( g_warriorFreshTopologyThisRun )
g_voteArrows . Discard ( " a member rebuilt from scratch this run - these arrows belong to a model that no longer exists " ) ;
else
g_voteArrows . Load ( ) ;
2026-07-14 22:36:27 -04:00
// Initialization successful
PrintVerbose ( " Initialization successful " ) ;
refactor(init): split OnInit's ~430-line boot sequence into named phases
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
2026-08-24 00:54:15 -04:00
FinalizeStartupUI ( functionName ) ;
feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00
//--- WHICH POSITION MODEL IS ACTUALLY LIVE. Allow_Hedging is a request, not a guarantee: on a NETTING
//--- account the second book cannot exist, and silently running the single-position path while the
//--- input reads "true" is exactly the kind of gap that costs three wrong diagnoses later. Say it once.
if ( Allow_Hedging & & ! WarriorHedgingActive ( ) )
PrintFormat ( " %s: Allow_Hedging is ON but this is a NETTING account (ACCOUNT_MARGIN_MODE=%d) - "
" running the single-position path. At most one position on %s, opposite votes "
" ignored. Nothing to fix in the EA; the account type decides this. " ,
functionName , ( int ) AccountInfoInteger ( ACCOUNT_MARGIN_MODE ) , _Symbol ) ;
else
if ( WarriorHedgingActive ( ) )
PrintFormat ( " %s: TWO BOOKS live on %s - long book magic %I64u, short book magic %I64u, at most "
" one position each. An opposite vote OPENS the other book rather than closing "
" this one, so both positions keep the certification they were deployed under. "
" The vote exit is pinned shut (threshold %d, unreachable). " ,
functionName , _Symbol , WarriorBookMagic ( true ) , WarriorBookMagic ( false ) ,
VOTE_EXIT_DISABLED_THRESHOLD ) ;
2026-07-14 22:36:27 -04:00
g_lastAlgoTradingAllowed = ( bool ) TerminalInfoInteger ( TERMINAL_TRADE_ALLOWED ) & & ( bool ) MQLInfoInteger ( MQL_TRADE_ALLOWED ) ;
if ( ! g_lastAlgoTradingAllowed )
Print ( functionName + " : WARNING - AlgoTrading is currently disabled (terminal or EA); signals will still train but no orders will be sent until it is re-enabled " ) ;
return INIT_SUCCEEDED ;
}
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
//+------------------------------------------------------------------+
//| THE FOUR TESTER HANDLERS, AND WHERE EACH ONE ACTUALLY RUNS. |
//| |
//| OnTesterInit / OnTesterPass / OnTesterDeinit run in the CONTROLLING|
//| TERMINAL, once per optimization session - never on an agent, never |
//| once per pass. OnTester runs on the AGENT, at the end of each pass.|
//| Keeping them cheap matters for a different reason than OnDeinit |
//| does: the terminal blocks the whole optimization while they run. |
//+------------------------------------------------------------------+
2026-07-14 22:36:27 -04:00
int OnTesterInit ( )
{
IsBacktesting = true ;
return ( INIT_SUCCEEDED ) ;
}
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
//--- Fires in the controlling terminal after each pass, but ONLY for passes that shipped frame data
//--- via FrameAdd(). This EA never calls FrameAdd, so this is unreachable today and is declared for
//--- one reason: without it, adding any frame-sending code later silently drops every frame instead
//--- of failing loudly. It deliberately does NOTHING but drain - reading frames here would put
//--- per-pass work on the terminal's critical path, which is exactly what stalls an optimization.
void OnTesterPass ( )
{
}
2026-07-14 22:36:27 -04:00
void OnTesterDeinit ( )
{
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
//--- Fires once at the END OF THE SESSION, not per pass - and by the time it runs, every pass's
//--- own OnDeinit() has already executed on its agent (including that pass's dbm.Deinit()).
//--- Nothing to tear down here: this process never opened a database, a chart or a model.
2026-07-26 21:09:53 -04:00
IsBacktesting = false ;
}
//+------------------------------------------------------------------+
//| Tester function for smooth linear equity optimization |
//| Output Range: 0.0 (Worst/Failed) to 100.0 (Perfect Linear Curve) |
//+------------------------------------------------------------------+
double OnTester ( )
{
// 1. Enforce minimum performance thresholds
2026-07-26 21:15:57 -04:00
double totalTrades = TesterStatistics ( STAT_TRADES ) ;
2026-07-26 21:09:53 -04:00
if ( totalTrades < 30 ) return ( 0.0 ) ;
double netProfit = TesterStatistics ( STAT_PROFIT ) ;
if ( netProfit < = 0 ) return ( 0.0 ) ;
double maxDrawdownPct = TesterStatistics ( STAT_EQUITY_DDREL_PERCENT ) ;
if ( maxDrawdownPct > 15.0 ) return ( 0.0 ) ;
// 2. Extract key performance components for linearity proxy
double profitFactor = TesterStatistics ( STAT_PROFIT_FACTOR ) ;
double recoveryFactor = TesterStatistics ( STAT_RECOVERY_FACTOR ) ;
double sharpeRatio = TesterStatistics ( STAT_SHARPE_RATIO ) ;
if ( profitFactor < = 0 | | recoveryFactor < = 0 ) return ( 0.0 ) ;
if ( sharpeRatio < 0 ) sharpeRatio = 0.01 ;
// 3. Trade density multiplier
double tradeDensity = 1.0 - MathExp ( -0.01 * ( double ) totalTrades ) ;
// 4. Mathematical combination proxy targeting visual linearity
double rawScore = profitFactor * recoveryFactor * sharpeRatio ;
// FIX: Convert percent to decimal (divide by 100) before penalization
double drawdownDecimal = maxDrawdownPct / 100.0 ;
rawScore / = ( 1.0 + ( drawdownDecimal * 0.5 ) ) ;
// Apply trade density
rawScore * = tradeDensity ;
// 5. Normalize the score to a 0.0 - 100.0 scale
double finalScore = 100.0 * ( 1.0 - MathExp ( -0.12 * rawScore ) ) ;
if ( finalScore > 100.0 ) finalScore = 100.0 ;
if ( finalScore < 0.0 ) finalScore = 0.0 ;
return ( finalScore ) ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Maps a terminal deinit reason code to a short label for logging, |
2026-07-14 22:36:27 -04:00
//| so operators can tell a routine recompile/parameter change apart |
//| from a terminal shutdown or the EA actually being removed. |
//+------------------------------------------------------------------+
string DeinitReasonToString ( const int reason )
{
switch ( reason )
{
case REASON_PROGRAM : return " EA stopped by ExpertRemove()/self " ;
case REASON_REMOVE : return " EA removed from chart " ;
case REASON_RECOMPILE : return " EA recompiled " ;
case REASON_CHARTCHANGE : return " chart symbol/period changed " ;
case REASON_CHARTCLOSE : return " chart closed " ;
case REASON_PARAMETERS : return " input parameters changed " ;
case REASON_ACCOUNT : return " account changed " ;
case REASON_TEMPLATE : return " template applied " ;
case REASON_INITFAILED : return " OnInit() failed " ;
case REASON_CLOSE : return " terminal closed " ;
default : return " unknown ( " + IntegerToString ( reason ) + " ) " ;
}
}
void OnDeinit ( const int reason )
{
2026-07-27 15:52:39 -04:00
static bool s_deinitInProgress = false ;
if ( s_deinitInProgress )
return ;
s_deinitInProgress = true ;
// Stop timer callbacks first so no more periodic work is queued while teardown runs.
EventKillTimer ( ) ;
2026-07-14 22:36:27 -04:00
string reasonStr = DeinitReasonToString ( reason ) ;
2026-07-27 15:52:39 -04:00
bool isTesterRun = ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) ) ;
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
//--- TESTER/OPTIMIZER FAST PATH. Everything between here and the shared teardown below exists to
//--- leave a CHART clean and a live model's state on disk. An optimization agent has neither: no
//--- visible chart to purge, no arrows worth persisting, and no weights to save (a tester run is
//--- inference-only by architecture - see m_inferenceOnly - so the weights are exactly what it
//--- loaded). Doing it anyway costs a per-signal arrow-sidecar WRITE plus two full chart-object
//--- scans on EVERY pass, which at optimization scale is hundreds of thousands of pointless file
//--- writes per agent and is the shape of thing that runs an agent into MetaTrader's ~4,500 ms
//--- deinit budget. Deliberately does NOT skip Expert.Deinit()/dbm.Deinit(): those free the signal
//--- tree and close any handle this pass opened, and leaking either across passes is how an agent
//--- accumulates its way into a stall.
if ( isTesterRun )
{
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
//--- THE PASS'S OWN PROFILE - the answer to "where did this pass's hours go", printed by the
//--- pass itself. See the g_tp* globals above OnTick().
if ( g_tpTicks > 0 )
PrintFormat ( " tester pass profile: %I64d tick(s) - pre(risk/algo/autosave) %.1fs (%.1f us/tick), "
" Expert.OnTick %.1fs (%.1f us/tick), journal %.1fs (%.1f us/tick) | %I64d timer "
" event(s) - %.1fs total. The biggest bucket is where the next optimization "
" second goes. " ,
g_tpTicks ,
g_tpPreUs / 1.0e6 , ( double ) g_tpPreUs / g_tpTicks ,
g_tpExpertUs / 1.0e6 , ( double ) g_tpExpertUs / g_tpTicks ,
g_tpJournalUs / 1.0e6 , ( double ) g_tpJournalUs / g_tpTicks ,
g_tpTimers , g_tpTimerUs / 1.0e6 ) ;
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . MarkShutdown ( ) ;
//--- Discard any in-flight era rather than finalising it - the same call the live path makes,
//--- and the reason a killed pass never leaves a half-written era behind.
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . FlushTrainRun ( ) ;
g_aiSignalCount = 0 ;
dbm . Deinit ( ) ;
Expert . Deinit ( ) ;
s_deinitInProgress = false ;
return ;
}
Print ( __FUNCTION__ + " : shutting down - reason: " + reasonStr ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//--- NOTHING BEFORE THE VISIBLE CLEANUP MAY TOUCH THE DISK. Measured 2026-08-25 18:23 (terminal
//--- close, six charts): two charts printed the line above and then NOTHING for 5.9 s until
//--- "Abnormal termination" - killed inside the two file writes that used to sit here
//--- (SaveSignalsVisibilityState + the un-split vote-arrow save) while the four sibling charts
//--- flooded the same disk with their own saves. Every leftover object the operator saw traces to
//--- that: the purge never ran because a WRITE ahead of it blocked. So the order is now: capture
//--- what needs the chart (in memory), clean the chart, and only then open a single file.
ulong deinitT0 = GetMicrosecondCount ( ) ;
2026-07-25 01:07:21 -04:00
ClearStatusLabel ( ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//--- THE VOTE ARROWS' SCAN HALF ONLY - before any purge removes the objects it reads from. This
//--- is the layer the chart shows with DrawUnfilteredSignals off, and it cannot be re-derived on
//--- the next attach without a full replay. The DISK half (WriteSnapshot) runs after the chart is
//--- clean, with the other persistence.
g_voteArrows . Snapshot ( ) ;
//--- EARLY VISIBLE-UI SWEEP, 2026-08-16. Removes the vote arrows just captured above.
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:
1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
id 1 and handled id 1001, and CExpertCustom broadcasts every chart
event to every filter - so each posted event ran a train chunk in ALL
N members (N*N chunks per round) and the chart thread never idled
long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
OnChartEventHandler. Fix: per-instance study-event ids
(STUDY_EVENT_ID_BASE + construction order, offset above the Controls
library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
double-clicks fired training chunks). ArmStudyEvent() is the single
post site; lost-event watchdog replaces the accidental
sibling-clears-my-flag rescue.
2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
identical features/labels, and it ends in the full MI diagnostic
suite, which the MI-share gate never intercepted on the sweep path -
four members ran four identical ~36s sweep+report blocks. First
member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
the rest apply it and skip both.
3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
autosave in flight ate it, OnDeinit got ~430ms and died in the first
member's arrow persist ("Abnormal termination" 432ms in). Fix: early
visible-UI sweep (native prefix deletes for status/panel/dialog)
right after ClearStatusLabel, and a fast path for still-training
models - their arrows are re-rendered every era, so they get one bulk
purge instead of scan+atomic-write in the death window.
4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
by era together; a member ahead of the slowest still-training member
declines Train() calls and its chunk budget is donated
(TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
the last member to finish the era scores the averaged vote vs the
mirrored Min_Vote_Open against the same target-before-stop outcomes
members grade themselves on, publishing an "Ensemble vote" line on
the aggregated panel. Member headlines now carry their lifetime win
rate with break-even.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
int earlySweepLeft = 0 ;
WarriorPurgeChartObjects ( 0 , true , earlySweepLeft ) ;
fix(deinit): a full model write was running ahead of the cheap cleanup
"Abnormal termination" is back, and this time it is not the arrows. The
timing names the culprit exactly:
16:02:31.547 OnDeinit: shutting down
16:02:36.003 Abnormal termination <- 4.46 s, MetaTrader gave up
16:02:36.226 chart signals - persisted <- cleanup finished 0.2 s LATE
OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining()
finalises an in-flight run, and FinalizeTrainRun() restores the best
checkpoint and then persists it - a full ~1MB model write per signal. So
the expensive step ran ahead of the cheap bounded one, which is precisely
the inversion the shutdown ordering exists to prevent. The previous fix
put PersistWeightsOnShutdown last and missed that StopTraining smuggles a
second save in at the front.
Two changes:
Cleanup now runs FIRST, then StopTraining, then the weight save. The
visible teardown is cheap and bounded, so it always completes even when
everything after it is killed.
And the deploy-persist inside FinalizeTrainRun is suppressed during
shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint
is already the live net by that line, and PersistWeightsOnShutdown writes
exactly those weights moments later. The old path wrote the same model
twice per signal - eight full writes across four charts - for no benefit.
A user-pressed Stop still persists immediately, because nothing else
would.
Compiles 0 errors / 0 warnings. Build tag deinit-order-v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . MarkShutdown ( ) ;
fix: the Adam second moment was never Adam - all four tiers
Root cause of the B=32 regression, and it predates F4 entirely. Every Adam
kernel stored v already square-rooted and then fed that stored value back in
as if it were the variance:
v_new = sqrt(b2 * v_old + (1 - b2) * g^2)
That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below
unit scale, so the denominator stops tracking the gradient and Adam degrades
into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll
(batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a
constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where
a scale-invariant optimizer gives the same distance for both. After the fix
all six magnitudes read 1.199 and v tracks |g| exactly.
It hit conv/LSTM specifically because they sit behind a batch-norm with
running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep
in the degraded regime - while the dense stack near the loss stayed in the
working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/
0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already
squared v back for gamma/beta and its comment named the kernels as wrong,
which is exactly why gamma/beta kept training while the stages behind froze.
Persisted .nnw needs no migration - v keeps its std-dev meaning.
Also, the two ways F4 exposed it, both mine:
- No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods
(Krizhevsky 2014; Granziol et al. 2022), applied once in
InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD.
- Plateau patience denominated in eras, so raising B made the ladder 32x more
impatient in its only unit. PAI converged at era 41 on ~49k updates where
the same config had been finding new bests at era 1028.
TrainPlateauPatienceEras() stretches it by the same sqrt(B).
TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23
eras per stage, not 8 -> 45). Both helpers are identities at B=1.
Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The
perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy
alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into
the ranking key rather than checked at deploy time so a one-sided era cannot
become best-so-far in the first place.
Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded
CAppDialog teardown that sat ahead of it - the same ordering inversion the
rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit
(vs ~1.1 s for the three that finished) having reached none of its cleanup,
so its arrows stayed on the chart. Steps are now timed in the log.
PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot
as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now
walks every object type and reports the object counts when both are zero.
Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt.
FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
//--- Destroy the control panel's own UI so CAppDialog removes its own objects cleanly (see
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//--- Controls\Dialog.mqh). BEFORE the per-member sidecar saves (file writes) now: CAppDialog names
//--- its objects with a numeric instance id, not a Warrior prefix, so a panel that outlives a
//--- force-kill is the one ghost no prefix sweep can ever remove (see PurgeOrphanedPanelObjects) -
//--- it must go while the budget is still certain.
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
//--- No isTesterRun guard needed any more: a tester run returned at the fast path above.
ExtPanel . Destroy ( reason ) ;
if ( g_altMapDialogOpen )
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
{
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
g_altMapDialog . Destroy ( reason ) ;
g_altMapDialogOpen = false ;
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
}
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
ulong deinitTVisual = GetMicrosecondCount ( ) ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
//--- Persist the drawn arrows to their sidecar and take them off the chart, on EVERY deinit
//--- reason. They come back on the next attach, from disk, if a model for that config exists -
//--- see PersistAndClearChartSignals(). The first file I/O of the teardown, and it runs with
//--- the label, panel and vote arrows already gone: with the raw view off (the shipped
//--- default) there are zero member arrows on the chart, so this is a scan plus an empty write.
g_aiSignals [ i ] . ShutdownChartCleanup ( ) ;
2026-08-22 00:25:52 -04:00
//--- FINAL SWEEP, after every owner-driven teardown has had its turn. Cheap and bounded: three
//--- prefix deletes plus one object-list scan, which is the shape of work this ordering rule
//--- permits at this point. It closes the ordinary case; OnInit closes the case where MetaTrader
//--- never let us finish.
fix: purge every EA object namespace on init and after deinit teardown
Leftover objects survived deinit because the cleanup list had drifted.
PurgeChart()'s own comment said it removed "our namespaced signal arrows
plus the status-label objects" while the code removed arrows ONLY, and
the panel prefix was swept at OnInit and nowhere else - so an ordinary
deinit left the status line, and any panel straggler, on the chart.
Three scattered call sites and a comment cannot be kept in step. There is
now ONE list - WarriorChartPrefixes() - covering arrows, status label and
panel, and one sweep, WarriorPurgeChartObjects(), used by every path.
Add a prefix there when a new object family appears and every cleanup
picks it up.
Two call sites added:
OnInit, before ANYTHING is drawn (including the status label it would
otherwise delete). Chart objects live in the chart PROFILE, not in the
EA, so they outlive the process: a deinit force-terminated at
MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5
replaced while attached all strand objects no later deinit will ever
own - and deleting the EA's files does not remove them, which is why
they read as corruption. Arrows are included: LoadChartSignals restores
them from their sidecar moments later and already opens with its own
arrow sweep, so this only removes orphans the sidecar does not account
for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds
that sidecar by scanning the chart.
OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control
tree and ClearStatusLabel clears text rather than guaranteeing object
removal; either can leave a straggler and nothing looked afterwards.
Bounded work - three prefix deletes and one object-list scan - so it
respects the ordering rule that keeps the cheap visible cleanup ahead
of the heavy save. Arrows excluded: ShutdownChartCleanup already
persisted and removed them and re-deleting would race that write.
The two are complementary: the deinit sweep closes the ordinary case, the
OnInit purge closes the case where MetaTrader never let us finish. Only
the second can help after a starved shutdown.
Both sweeps rescan by name across EVERY object type and delete what the
bulk call missed. ObjectsDeleteAll's return has already been observed
disagreeing with a by-name scan of the same chart microseconds apart, and
object commands are queued on the chart rather than applied inline, so a
returned count is not evidence the objects are gone.
Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so
the name cannot drift away from the list that cleans it up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
int deinitLeftover = 0 ;
int deinitPurged = WarriorPurgeChartObjects ( 0 , true , deinitLeftover ) ;
if ( deinitPurged > 0 )
PrintFormat ( " %s: final sweep removed %d EA object(s) that survived their own teardown%s. " ,
__FUNCTION__ , deinitPurged ,
( deinitLeftover > 0
? StringFormat ( " (%d needed a by-name delete) " , deinitLeftover ) : " " ) ) ;
ChartRedraw ( 0 ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
ulong deinitTArrows = GetMicrosecondCount ( ) ;
Print ( __FUNCTION__ + " : cleanup timings - visuals " + DoubleToString ( ( deinitTVisual - deinitT0 ) / 1000.0 , 0 ) +
" ms, member arrows " + DoubleToString ( ( deinitTArrows - deinitTVisual ) / 1000.0 , 0 ) +
fix: the Adam second moment was never Adam - all four tiers
Root cause of the B=32 regression, and it predates F4 entirely. Every Adam
kernel stored v already square-rooted and then fed that stored value back in
as if it were the variance:
v_new = sqrt(b2 * v_old + (1 - b2) * g^2)
That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below
unit scale, so the denominator stops tracking the gradient and Adam degrades
into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll
(batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a
constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where
a scale-invariant optimizer gives the same distance for both. After the fix
all six magnitudes read 1.199 and v tracks |g| exactly.
It hit conv/LSTM specifically because they sit behind a batch-norm with
running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep
in the degraded regime - while the dense stack near the loss stayed in the
working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/
0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already
squared v back for gamma/beta and its comment named the kernels as wrong,
which is exactly why gamma/beta kept training while the stages behind froze.
Persisted .nnw needs no migration - v keeps its std-dev meaning.
Also, the two ways F4 exposed it, both mine:
- No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods
(Krizhevsky 2014; Granziol et al. 2022), applied once in
InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD.
- Plateau patience denominated in eras, so raising B made the ladder 32x more
impatient in its only unit. PAI converged at era 41 on ~49k updates where
the same config had been finding new bests at era 1028.
TrainPlateauPatienceEras() stretches it by the same sqrt(B).
TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23
eras per stage, not 8 -> 45). Both helpers are identities at B=1.
Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The
perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy
alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into
the ranking key rather than checked at deploy time so a one-sided era cannot
become best-so-far in the first place.
Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded
CAppDialog teardown that sat ahead of it - the same ordering inversion the
rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit
(vs ~1.1 s for the three that finished) having reached none of its cleanup,
so its arrows stayed on the chart. Steps are now timed in the log.
PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot
as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now
walks every object type and reports the object counts when both are zero.
Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt.
FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
" ms. MetaTrader force-terminates OnDeinit at roughly 4,500 ms TOTAL; if this line is missing "
" entirely, the budget expired before it and the step that overran is the one after the last "
" message that DID print. " ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//--- THE DISK, from here on - the chart is already clean whatever happens below. Small writes
//--- first, the weight save last.
SaveSignalsVisibilityState ( ) ;
if ( g_voteArrows . WriteSnapshot ( ) )
PrintVerbose ( __FUNCTION__ + " : persisted " + IntegerToString ( g_voteArrows . LastSaved ( ) ) + " combined-vote arrows " ) ;
fix: flush the in-flight era on shutdown; sweep orphaned chart objects on attach
Chart objects live in the MT5 chart PROFILE, not in this EA's files.
They survive a terminal restart, a recompile, and deleting every
.nnw/.cfg/.stats/.arrows on disk. Only a deinit that RUNS TO COMPLETION
removes them - and MetaTrader force-terminates OnDeinit at roughly
4,500 ms, so a run killed mid-cleanup orphans them permanently with no
owner left to clean up after. That is the "deleted every file,
recompiled, restarted, old arrows and a stale panel still there"
report: nothing was wrong with the files and deleting them could not
have helped.
Both halves are fixed.
STOP OVERRUNNING THE BUDGET. OnDeinit used to finalise the in-flight
run (StopTraining -> FinalizeTrainRun: checkpoint restore, live-state
re-seed) and then write two full nets per chart. On four charts that is
the bulk of the budget, spent to preserve a PARTIAL era that was never
scored, never checkpointed and never deployable. FlushTrainRun()
discards it instead - drop the resumable bookkeeping, leave the net
neutral (unfreeze BN, flush the batch, batch size 1), skip the save -
and training resumes from the last completed era, which the era-end
save and the periodic autosave have already put on disk. What is
discarded is bounded by one era.
A CONVERGED model keeps the old finalise-and-save path: its weights can
carry online-learning updates made since the last era boundary, and for
a deployed model no further era boundary is coming to persist them.
MAKE CLEANUP SELF-HEALING. Every purge sat behind a branch - no model
loaded, sidecar missing - so the common paths returned leaving whatever
the previous instance stranded. LoadChartSignals now sweeps the arrow
namespace unconditionally before restoring, so the post-init chart
holds exactly what the sidecar holds whichever branch runs, and the
panel gets the same treatment before Create() (CAppDialog namespaces
its controls, so a killed Destroy strands the lot and the next attach
draws a second panel on the corpse).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:08:40 -04:00
bool flushedAny = false ;
fix(deinit): a full model write was running ahead of the cheap cleanup
"Abnormal termination" is back, and this time it is not the arrows. The
timing names the culprit exactly:
16:02:31.547 OnDeinit: shutting down
16:02:36.003 Abnormal termination <- 4.46 s, MetaTrader gave up
16:02:36.226 chart signals - persisted <- cleanup finished 0.2 s LATE
OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining()
finalises an in-flight run, and FinalizeTrainRun() restores the best
checkpoint and then persists it - a full ~1MB model write per signal. So
the expensive step ran ahead of the cheap bounded one, which is precisely
the inversion the shutdown ordering exists to prevent. The previous fix
put PersistWeightsOnShutdown last and missed that StopTraining smuggles a
second save in at the front.
Two changes:
Cleanup now runs FIRST, then StopTraining, then the weight save. The
visible teardown is cheap and bounded, so it always completes even when
everything after it is killed.
And the deploy-persist inside FinalizeTrainRun is suppressed during
shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint
is already the live net by that line, and PersistWeightsOnShutdown writes
exactly those weights moments later. The old path wrote the same model
twice per signal - eight full writes across four charts - for no benefit.
A user-pressed Stop still persists immediately, because nothing else
would.
Compiles 0 errors / 0 warnings. Build tag deinit-order-v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
fix: flush the in-flight era on shutdown; sweep orphaned chart objects on attach
Chart objects live in the MT5 chart PROFILE, not in this EA's files.
They survive a terminal restart, a recompile, and deleting every
.nnw/.cfg/.stats/.arrows on disk. Only a deinit that RUNS TO COMPLETION
removes them - and MetaTrader force-terminates OnDeinit at roughly
4,500 ms, so a run killed mid-cleanup orphans them permanently with no
owner left to clean up after. That is the "deleted every file,
recompiled, restarted, old arrows and a stale panel still there"
report: nothing was wrong with the files and deleting them could not
have helped.
Both halves are fixed.
STOP OVERRUNNING THE BUDGET. OnDeinit used to finalise the in-flight
run (StopTraining -> FinalizeTrainRun: checkpoint restore, live-state
re-seed) and then write two full nets per chart. On four charts that is
the bulk of the budget, spent to preserve a PARTIAL era that was never
scored, never checkpointed and never deployable. FlushTrainRun()
discards it instead - drop the resumable bookkeeping, leave the net
neutral (unfreeze BN, flush the batch, batch size 1), skip the save -
and training resumes from the last completed era, which the era-end
save and the periodic autosave have already put on disk. What is
discarded is bounded by one era.
A CONVERGED model keeps the old finalise-and-save path: its weights can
carry online-learning updates made since the last era boundary, and for
a deployed model no further era boundary is coming to persist them.
MAKE CLEANUP SELF-HEALING. Every purge sat behind a branch - no model
loaded, sidecar missing - so the common paths returned leaving whatever
the previous instance stranded. LoadChartSignals now sweeps the arrow
namespace unconditionally before restoring, so the post-init chart
holds exactly what the sidecar holds whichever branch runs, and the
panel gets the same treatment before Create() (CAppDialog namespaces
its controls, so a killed Destroy strands the lot and the next attach
draws a second panel on the corpse).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:08:40 -04:00
{
if ( g_aiSignals [ i ] . TrainingComplete ( ) )
g_aiSignals [ i ] . StopTraining ( ) ;
else
flushedAny = ( g_aiSignals [ i ] . FlushTrainRun ( ) | | flushedAny ) ;
}
if ( flushedAny )
Print ( __FUNCTION__ + " : discarded the in-flight era on at least one model rather than finalising it - "
" training resumes from the last completed era, which is already on disk. This is what keeps "
" the chart cleanup inside MetaTrader's deinit budget. " ) ;
2026-08-22 00:25:52 -04:00
//--- Persist every active AI signal's current in-memory weights/state, so a terminal restart,
//--- recompile, chart re-add or template swap resumes from here rather than from the last fully-
//--- completed training era only.
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
2026-07-27 15:52:39 -04:00
{
perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
if ( ! g_aiSignals [ i ] . TrainingComplete ( ) )
continue ;
if ( ! g_aiSignals [ i ] . PersistWeightsOnShutdown ( ) )
Print ( __FUNCTION__ + " : WARNING - failed to persist weights for signal index " + IntegerToString ( i ) + " on shutdown (reason: " + reasonStr + " ) " ) ;
2026-07-27 15:52:39 -04:00
}
2026-07-14 22:36:27 -04:00
g_aiSignalCount = 0 ;
dbm . Deinit ( ) ;
Expert . Deinit ( ) ;
2026-07-25 01:07:21 -04:00
//--- belt-and-suspenders: the status label was already cleared first, but re-clear in case a later path
//--- (e.g. Expert.Deinit's destructors) drew anything, so nothing is left on the chart after removal.
2026-07-17 21:28:59 -04:00
ClearStatusLabel ( ) ;
2026-07-27 15:52:39 -04:00
s_deinitInProgress = false ;
2026-07-14 22:36:27 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//--- Expert.OnTimer() does the (comparatively expensive) DB-ranking work, originally paced by its
2026-08-01 11:27:28 -04:00
//--- own 1-hour EventSetTimer() interval; now that the timer fires every 500ms (see OnInit for why
//--- that interval), pace that work manually instead so it still only actually runs about once an hour.
2026-07-14 22:36:27 -04:00
# define DB_RANKING_INTERVAL_SECONDS 3600
datetime g_lastDbRankingRun = 0 ;
2026-08-22 00:25:52 -04:00
//--- Alt-data maintenance (see System\AltDataFetch.mqh): the EA downloads its own missing history
//--- at attach time and keeps appending forward while deployed, so online learning never depends on
//--- an external process.
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//+------------------------------------------------------------------+
//| Unknown chart symbol -> ask which instrument it is, once. |
//| Non-blocking: the EA keeps initialising, training and trading |
//| while the dialog sits on the chart. An unmapped symbol just |
//| contributes 0 alt-data features. |
//+------------------------------------------------------------------+
void MaybeAskAltDataMapping ( void )
{
if ( ! EnableAltData | | g_altMapAsked | | g_altMapDialogOpen )
return ;
if ( ! g_altDataFetch . NeedsMapping ( _Symbol ) )
return ; // catalogued, or the user already recorded a choice
g_altMapAsked = true ; // one prompt per attach, even if it is closed unanswered
int n = g_altDataFetch . CatalogCount ( ) ;
string labels [ ] , canon [ ] ;
ArrayResize ( labels , n ) ;
ArrayResize ( canon , n ) ;
for ( int i = 0 ; i < n ; i + + )
{
labels [ i ] = g_altDataFetch . CatalogLabel ( i ) ;
canon [ i ] = g_altDataFetch . CatalogName ( i ) ;
}
PrintFormat ( " AltDataFetch: '%s' is not in the alt-data catalog - asking which instrument it maps "
" to. The EA runs normally either way; the answer is saved in symbol_map.cfg. " , _Symbol ) ;
if ( g_altMapDialog . Show ( _Symbol , labels , canon ) )
g_altMapDialogOpen = true ;
else
Print ( " AltDataFetch: the mapping dialog could not be opened - map it by hand instead: add a "
" line like ' " + _Symbol + " =SP500' to Common \\ Files \\ Warrior_EA \\ AltData \\ symbol_map.cfg "
" (or ' " + _Symbol + " =NONE' to decline). " ) ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Apply the dialog's answer once the user has clicked. Called from |
//| OnChartEvent, after the dialog has seen the same event. Drive |
//| the FILTERED view's historical reconstruction. |
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
//+------------------------------------------------------------------+
# define OVERLAY_BARS_PER_SLICE 150
2026-08-22 00:25:52 -04:00
//--- Minimum wall-clock between overlay re-arms. Five minutes keeps the reconstruction current on
//--- any human timescale while cutting the churn ~15x.
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
# define OVERLAY_REARM_MIN_MS 60000
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
uint g_lastOverlayArmTick = 0 ;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- Longest a sweep waits for a member that has stopped producing snapshots. Past this it draws
//--- with whoever is ready AND SAYS SO - a stale chart that never redraws is worse than a partial
//--- one that reports itself.
# define OVERLAY_PARTIAL_ARM_MS 600000
//+------------------------------------------------------------------+
//| Which ensemble members must have a snapshot before a sweep runs. |
//| |
//| The array index is the member's m_ensembleIndex, so the slot IS |
//| the bit. Zero means no ensemble is registered (a single-model |
//| configuration), and the gate is then a no-op rather than a lock. |
//+------------------------------------------------------------------+
uint EnrolledOverlayMask ( void )
{
uint mask = 0 ;
int n = MathMin ( ArraySize ( g_warriorEnsemble ) , ENS_MAX_MEMBERS ) ;
for ( int i = 0 ; i < n ; i + + )
if ( CheckPointer ( g_warriorEnsemble [ i ] ) ! = POINTER_INVALID )
mask | = ( ( ( uint ) 1 ) < < i ) ;
return mask ;
}
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 AdvanceFilteredSignalOverlay ( void )
{
if ( DrawUnfilteredSignals )
return ; // raw view owns the chart; nothing to reconstruct
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- RESTORE BEFORE RECONSTRUCT. The sidecar holds the arrows the previous session actually drew;
//--- the sweep below can only rebuild them while members still publish era-end snapshots, which a
//--- deployed ensemble no longer does. Draining the queue first means a reloaded chart shows its
//--- history immediately instead of waiting on a sweep that may never be armed.
if ( g_voteArrows . Pending ( ) )
{
g_voteArrows . AdvanceRestore ( ) ;
return ; // one chart-drawing job at a time - both are budgeted per slice
}
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
//--- ARM ON THE MEMBERS' OWN SIGNAL, not on an era-counter diff. g_warriorOverlayArmRequest is
//--- set by RankTiersFromOos() at pass-3 completion - the moment a member's era-end snapshot
2026-08-22 00:25:52 -04:00
//--- became fresher - which is the only event a redraw can act on.
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
bool wantArm = g_warriorOverlayArmRequest | | ( g_aiSignalCount = = 0 & & g_lastOverlayArmTick = = 0 ) ;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- EVERY ENROLLED MEMBER FIRST. A member still mid-era has no snapshot, the sweep skips it
//--- before the divisor, and the remaining member's own tier weight becomes the whole vote - a
//--- consensus arrow drawn from one model. See g_warriorOverlayReadyMask.
uint enrolled = EnrolledOverlayMask ( ) ;
bool everyoneReady = ( enrolled = = 0 ) | | ( ( g_warriorOverlayReadyMask & enrolled ) = = enrolled ) ;
if ( wantArm & & ! everyoneReady )
{
if ( g_warriorOverlayArmSince = = 0 )
g_warriorOverlayArmSince = GetTickCount ( ) ;
//--- Bounded, so a member that stopped cannot freeze the chart. Drawing partial is allowed;
//--- drawing partial SILENTLY is not.
if ( GetTickCount ( ) - g_warriorOverlayArmSince > = OVERLAY_PARTIAL_ARM_MS )
{
PrintFormat ( " Warrior: filtered overlay drawing with a PARTIAL ensemble after %d s - members "
" ready 0x%X of 0x%X enrolled. The missing members have produced no era-end "
" snapshot, so they abstain from every bar in this sweep and the vote is "
" diluted toward the members that did finish. Expect fewer arrows than the live "
" vote would cast, not different ones. " ,
OVERLAY_PARTIAL_ARM_MS / 1000 , g_warriorOverlayReadyMask , enrolled ) ;
everyoneReady = true ;
}
}
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
//--- Re-arm only between sweeps (mid-flight restart would strand the tail undrawn), and at most
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- once a minute once everyone is in.
if ( wantArm & & everyoneReady & & ! Expert . FilteredOverlayPending ( )
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
& & ( g_lastOverlayArmTick = = 0 | | GetTickCount ( ) - g_lastOverlayArmTick > = OVERLAY_REARM_MIN_MS ) )
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): 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
g_warriorOverlayArmRequest = false ;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
g_warriorOverlayReadyMask = 0 ;
g_warriorOverlayArmSince = 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
g_lastOverlayArmTick = GetTickCount ( ) ;
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
Expert . ArmFilteredOverlay ( ) ;
}
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- MIRROR THE CHART THE MOMENT A SWEEP FINISHES, not only at shutdown. A sweep is the only thing
//--- that rewrites this layer wholesale, so its completion is exactly when the file is stale - and
//--- saving here means a terminal that is killed rather than closed still leaves a good record.
bool sweepWasPending = Expert . FilteredOverlayPending ( ) ;
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
Expert . AdvanceFilteredOverlay ( OVERLAY_BARS_PER_SLICE ) ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
if ( sweepWasPending & & ! Expert . FilteredOverlayPending ( ) )
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
{
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
g_voteArrows . Save ( ) ;
feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00
//--- BACKFILL AN EMPTY COMBINED-VOTE RECORD from the sweep's own arithmetic. The record
//--- (g_ensCumOosCorrect/Total) normally accrues once per era at pass-3 completion - and a
//--- deployed ensemble runs no further eras, so a chart whose .stats predate the record (the
//--- WST7 migration) kept "Vote win rate: measuring..." forever even after the replay pass
//--- rebuilt every member's ladder (user report 2026-08-25). The sweep scores exactly the
//--- population the record describes: bars whose reconstructed vote cleared the open threshold
//--- under the direction policy, against the same inline swing-pivot labels the replay used.
//--- Guards: deployed only (a training-time sweep must not pre-empt the era scorer), and only
//--- into an EMPTY record (never on top of real era history - a restored record wins).
long overlayFired = 0 , overlayWins = 0 ;
if ( Expert . TakeOverlayVoteScore ( overlayFired , overlayWins )
& & overlayFired > 0 & & g_ensCumOosTotal < = 0 & & WarriorChartModelsDeployed ( ) )
{
g_ensCumOosCorrect = overlayWins ;
g_ensCumOosTotal = overlayFired ;
PublishEnsembleAccuracyLine ( -1.0 , 0 ) ;
PrintFormat ( " Warrior: combined-vote record backfilled from the reconstructed overlay - %d "
" call(s) at or above the %.0f%% threshold, %d correct (%d%%). Partly IN-SAMPLE "
" (the window includes bars the members trained on); the next genuine era-end "
" scoring pass supersedes it. " ,
( int ) overlayFired , g_ensembleVoteThreshold , ( int ) overlayWins ,
( int ) MathRound ( overlayWins * 100.0 / overlayFired ) ) ;
//--- Persist NOW, into every member's .stats (the loader adopts the most complete copy) -
//--- the whole class of bug this repairs is state that existed in memory and was never
//--- written down.
for ( int mi = 0 ; mi < ArraySize ( g_warriorEnsemble ) ; mi + + )
if ( CheckPointer ( g_warriorEnsemble [ mi ] ) ! = POINTER_INVALID )
g_warriorEnsemble [ mi ] . OnlineSaveModelStats ( ) ;
}
}
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
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
void PollAltDataMapDialog ( void )
{
if ( ! g_altMapDialogOpen | | ! g_altMapDialog . Done ( ) )
return ;
string choice = g_altMapDialog . Result ( ) ;
g_altDataFetch . SaveUserMapping ( _Symbol , choice ) ;
g_altMapDialog . Destroy ( REASON_REMOVE ) ;
g_altMapDialogOpen = false ;
if ( choice = = " NONE " )
{
Print ( " AltDataFetch: recorded 'no alternative data' for " + _Symbol + " - it will not ask "
" again. Remove that line from symbol_map.cfg to be asked at the next attach. " ) ;
return ;
}
//--- Download now rather than waiting up to 30 minutes for the next upkeep tick.
g_lastAltDataRun = TimeCurrent ( ) ;
if ( g_altDataFetch . Update ( _Symbol ) )
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . AltDataReload ( ) ;
Print ( " AltDataFetch: alt data for " + _Symbol + " is being maintained now. A model that was "
" already created without these features keeps its pinned input width - re-attach the EA "
" to build models that actually train on them. " ) ;
}
2026-07-14 22:36:27 -04:00
void OnTimer ( )
{
2026-08-22 00:25:52 -04:00
//--- STOP FIRST. Nothing below this line matters to a program that is being unloaded: the training
//--- poll, the alt-data upkeep (blocking WebRequests) and the DB ranking pass are all work whose
fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks
Two things, one of which was a compile error.
1. ExitPolicy() was declared in the protected block but is pushed in from
Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters.
2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured
from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot
begin until whatever is in flight returns - so a scan still running after
_StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge
never gets its turn.
New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress.
Deliberately NOT m_trainingStopRequested: that latches, and a latched flag
would permanently disable scans that must run again on the next Start.
Guarded, longest first:
- TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings
on the way out (best[] is mutated in place; the tuner otherwise keeps the
last trial's parameters, which nothing chose).
- ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so
m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar
read the last candidate's multiples as the configured geometry).
- ReportFeatureLabelInformation / ReportExcursionInformation / lag profile -
nulls ABANDON rather than truncate: fewer draws is not a smaller null, it
is a wrong one, and p shifts toward significance. m_dirEvidence staying
false is the safe direction.
- SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line
is dropped instead of latching a partial expectancy as the run's only report.
- ReportGeometryExpectancyScan - per ladder rung.
- HttpGet - one choke point for up to a dozen blocking WebRequests per
first-pass Update(). An in-flight request cannot be cancelled; refusing to
start another is the whole remedy.
- PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain -
entry points, so a queued event cannot open an era during teardown.
TuneIndicatorsAndTrain's guard is the first statement, ahead of the
m_tuneFilterDone / g_ensembleChartTuneDone latches.
- OnTick / OnTimer / OnChartEvent.
Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3
yield on a 120 ms budget); the warm-up scans did not, and they are the longest
uninterruptible stretches the EA has.
StopTraining() is unchanged: the operator's Stop still finalises synchronously.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
//--- results are about to be discarded.
if ( IsStopped ( ) )
return ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
ulong tpT0 = g_tpActive ? GetMicrosecondCount ( ) : 0 ;
if ( g_tpActive )
g_tpTimers + + ;
fix(pool): defer the atomic promotion to the timer instead of spinning on the tick
REPLACES the in-line retry from ad4ae58, which was the wrong shape and did not
work. Measured after deploying it:
atomic rename ... failed (error 5004) after 4 attempts
MQL5 exposes no FILE_SHARE_DELETE, so a rename CANNOT succeed while any reader
holds the destination open - it is not a lock that waiting longer wins. The
retry assumed a peer holds a pool file for "tens of ms"; USDCAD_16388.bin is
134 MB and a peer reading it holds the handle for SECONDS. The loop lost every
time and bought nothing but 75ms of tick latency on the failure path.
The content is already written and correct - only the SWAP is blocked. So try the
rename once, and on failure remember the temp and promote it from OnTimer, where
I/O belongs. Once the reader closes, a single FileMove lands it. That beats the
old fallback of waiting for the next full publish, which rewrites all 134 MB and
may be an era away.
* pending list is bounded (8) and deduplicated - AtomicWriteBegin reuses one
temp name per file, so a second failure for the same file must not take a
second slot. A full list falls back to the previous next-publish behaviour.
* a successful write FORGETS any queued promotion for that name, so a stale
temp can never overwrite fresher content.
* a vanished temp (a later publish succeeded outright) is dropped, not retried.
* a landed promotion is LOGGED. Silence is what made me misread the last
attempt as working when there had simply been no contention in the window.
Compiled clean; NOT yet run - and note that verification needs a collision to
occur, which happened ~27 times across a whole day. Absence of the message in any
one window is not evidence either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:41:35 -04:00
//--- ANY FILE WHOSE ATOMIC SWAP LOST A RACE. A rename fails while a peer chart holds the destination
//--- open, and with 134 MB pool files that read lasts seconds - far too long to wait out inside a
//--- quote. The content is already written; only the swap is outstanding. Here, on the clock, is
//--- where it costs nothing, and landing it now beats waiting for the next full publish to rewrite
//--- the whole file. No-op (one integer compare) whenever nothing is pending, which is nearly always.
AtomicPromotePending ( ) ;
2026-08-02 12:25:20 -04:00
//--- Also here, not only in OnTick(): this chart's own symbol can go minutes without a quote while
//--- an open position on ANOTHER symbol moves account equity through the limit. Equity is
//--- account-wide, so the budget must be re-checked on the clock, not only on this symbol's ticks.
g_riskBudget . Update ( ) ;
2026-07-14 22:36:27 -04:00
//--- keeps training progressing on wall-clock time even with no ticks at all (market closed) -
//--- OnTickHandler's own scheduling only ever runs when a tick actually arrives
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . PollTraining ( ) ;
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
//--- FILTERED VIEW: keep the reconstructed history in step with the models. One slice per timer
//--- tick, same cadence and same reasoning as the chunked signal rescan above it.
AdvanceFilteredSignalOverlay ( ) ;
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
//--- Keep the vote readout tracking the models at timer cadence - Direction() only runs on
//--- new-bar ticks (stock CExpert::Refresh gates it), which on H4 is once every four hours.
2026-08-25 22:51:50 -04:00
//--- NOT in the tester: this runs a real forward pass per ensemble member (ProspectiveVote ->
//--- DisplayInference -> Net.feedForward) whose only product is a chart HUD string, and a
//--- tester chart is already treated as throwaway elsewhere (see the vote-arrow layer skip
//--- in OnInit above).
if ( ! MQLInfoInteger ( MQL_TESTER ) & & ! MQLInfoInteger ( MQL_OPTIMIZATION ) )
Expert . RefreshVoteReadout ( ) ;
2026-07-26 12:52:56 -04:00
//--- Finish the Show Signals sequence once every instance queued by ToggleSignalsVisibility has
//--- drained its chunked rescan (each is advanced one slice per PollTraining call above).
FinalizeSignalsRescanIfDone ( ) ;
2026-08-22 00:25:52 -04:00
//--- Training can finish and deploy ITSELF (the plateau ladder finalising the best checkpoint,
//--- or the era-cap deploy) with no button ever pressed, and RefreshControlPanelLabels()
//--- otherwise only runs in response to a click - which would leave the panel offering "Deploy
//--- Model" on an already-deployed model until the user happened to click something.
2026-07-25 16:39:11 -04:00
bool deployedNow = AllTrainingDeployed ( ) ;
if ( deployedNow ! = g_lastDeployedState )
{
g_lastDeployedState = deployedNow ;
RefreshControlPanelLabels ( ) ;
}
2026-08-16 13:59:03 -04:00
//--- Alt-data upkeep, before the UseDatabaseRanking early-return so it runs regardless of
//--- that input. First pass backfills any missing history (one blocking WebRequest per stale
//--- source, seconds); steady state is two date compares every 30 minutes.
2026-08-25 22:51:50 -04:00
if ( ! MQLInfoInteger ( MQL_TESTER ) & & ! MQLInfoInteger ( MQL_OPTIMIZATION ) & & ! MQLInfoInteger ( MQL_FORWARD ) )
2026-08-16 13:59:03 -04:00
{
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
MaybeAskAltDataMapping ( ) ;
2026-08-16 13:59:03 -04:00
datetime altNow = TimeCurrent ( ) ;
if ( g_lastAltDataRun = = 0 | | altNow - g_lastAltDataRun > = ALTDATA_CHECK_SECONDS )
{
g_lastAltDataRun = altNow ;
if ( g_altDataFetch . Update ( _Symbol ) )
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . AltDataReload ( ) ;
}
}
2026-07-14 22:36:27 -04:00
if ( ! UseDatabaseRanking )
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
{
if ( g_tpActive )
g_tpTimerUs + = GetMicrosecondCount ( ) - tpT0 ;
2026-07-14 22:36:27 -04:00
return ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
}
2026-07-14 22:36:27 -04:00
datetime now = TimeCurrent ( ) ;
2026-08-16 21:08:41 -04:00
//--- One-shot bypass of the hourly throttle below (see g_forcePatternWeightsRefresh's declaration
//--- comment): a model that just finished its backfill has real DB history sitting unranked, and
//--- waiting up to an hour for it to reach UpdateSignalsWeights() is exactly the "not ready to trade
//--- the instant training finishes" gap this whole feature exists to close.
bool forceNow = g_forcePatternWeightsRefresh ;
if ( forceNow )
g_forcePatternWeightsRefresh = false ;
if ( ! forceNow & & g_lastDbRankingRun ! = 0 & & now - g_lastDbRankingRun < DB_RANKING_INTERVAL_SECONDS )
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
{
if ( g_tpActive )
g_tpTimerUs + = GetMicrosecondCount ( ) - tpT0 ;
2026-07-14 22:36:27 -04:00
return ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
}
2026-07-14 22:36:27 -04:00
g_lastDbRankingRun = now ;
Expert . OnTimer ( ) ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
if ( g_tpActive )
g_tpTimerUs + = GetMicrosecondCount ( ) - tpT0 ;
2026-07-14 22:36:27 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CheckAlgoTradingState ( void )
{
bool allowed = ( bool ) TerminalInfoInteger ( TERMINAL_TRADE_ALLOWED ) & & ( bool ) MQLInfoInteger ( MQL_TRADE_ALLOWED ) ;
if ( allowed ! = g_lastAlgoTradingAllowed )
{
if ( allowed )
Print ( __FUNCTION__ + " : AlgoTrading re-enabled - order placement resumed (training/signals were unaffected while disabled) " ) ;
else
Print ( __FUNCTION__ + " : AlgoTrading disabled (terminal toggle off, or EA's own permission revoked) - no new orders will be sent until re-enabled; training/signal generation continues unaffected " ) ;
g_lastAlgoTradingAllowed = allowed ;
}
}
void AutosaveWeightsIfDue ( void )
{
2026-08-22 00:25:52 -04:00
//--- NEVER autosave inside the Strategy Tester / optimizer. The whole reason this exists is that
//--- a live terminal can be killed without OnDeinit running - a tester run has no such exposure.
//--- A tester run is inference-only anyway (see m_inferenceOnly) - the weights never change, so
2026-07-26 10:59:46 -04:00
//--- there is literally nothing to persist.
if ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) )
return ;
2026-07-26 10:27:38 -04:00
datetime lastBarDate = ( datetime ) SeriesInfoInteger ( _Symbol , _Period , SERIES_LASTBAR_DATE ) ;
//--- <=0 is a transient history-sync hiccup, not "no new bar" - skip this tick and try again on the
//--- next one rather than risk locking onto a bad watermark (same guard philosophy as
//--- ScheduleTrainingIfNeeded's own lastBarDate read).
if ( lastBarDate < = 0 | | lastBarDate = = g_lastAutosaveBarTime )
2026-07-14 22:36:27 -04:00
return ;
2026-07-26 10:27:38 -04:00
g_lastAutosaveBarTime = lastBarDate ;
2026-07-14 22:36:27 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . SaveWeightsNow ( ) ;
}
void OnTick ( )
{
2026-08-22 00:25:52 -04:00
//--- STOP FIRST - same reasoning as OnTimer's guard. Deliberately AHEAD of the risk-budget update
//--- too: a program that is unloading places no orders, so there is nothing left for the budget to
//--- protect.
fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks
Two things, one of which was a compile error.
1. ExitPolicy() was declared in the protected block but is pushed in from
Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters.
2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured
from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot
begin until whatever is in flight returns - so a scan still running after
_StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge
never gets its turn.
New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress.
Deliberately NOT m_trainingStopRequested: that latches, and a latched flag
would permanently disable scans that must run again on the next Start.
Guarded, longest first:
- TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings
on the way out (best[] is mutated in place; the tuner otherwise keeps the
last trial's parameters, which nothing chose).
- ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so
m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar
read the last candidate's multiples as the configured geometry).
- ReportFeatureLabelInformation / ReportExcursionInformation / lag profile -
nulls ABANDON rather than truncate: fewer draws is not a smaller null, it
is a wrong one, and p shifts toward significance. m_dirEvidence staying
false is the safe direction.
- SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line
is dropped instead of latching a partial expectancy as the run's only report.
- ReportGeometryExpectancyScan - per ladder rung.
- HttpGet - one choke point for up to a dozen blocking WebRequests per
first-pass Update(). An in-flight request cannot be cancelled; refusing to
start another is the whole remedy.
- PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain -
entry points, so a queued event cannot open an era during teardown.
TuneIndicatorsAndTrain's guard is the first statement, ahead of the
m_tuneFilterDone / g_ensembleChartTuneDone latches.
- OnTick / OnTimer / OnChartEvent.
Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3
yield on a 120 ms budget); the warm-up scans did not, and they are the longest
uninterruptible stretches the EA has.
StopTraining() is unchanged: the operator's Stop still finalises synchronously.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
if ( IsStopped ( ) )
return ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
ulong tp0 = g_tpActive ? GetMicrosecondCount ( ) : 0 ;
2026-07-14 22:36:27 -04:00
CheckAlgoTradingState ( ) ;
2026-08-22 00:25:52 -04:00
//--- FIRST, and before Expert.OnTick() can open anything.
2026-08-02 12:25:20 -04:00
g_riskBudget . Update ( ) ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
//--- THE DERIVED VOTE THRESHOLD, before anything can act on the old one. The era verdict publishes
//--- g_ensDerivedThreshold; this is where it reaches the m_threshold_open the trade decision reads.
//--- Here rather than inside the signal because ExpertSignalCustom.mqh does not see the ensemble
//--- globals (it is the PARENT of the AI filter that declares them), and here rather than at init
//--- because the value does not exist until an era has been scored.
if ( g_ensDerivedThreshold > 0.0 )
Expert . PublishVoteThreshold ( ( int ) MathRound ( g_ensDerivedThreshold ) ) ;
2026-07-14 22:36:27 -04:00
AutosaveWeightsIfDue ( ) ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
ulong tp1 = g_tpActive ? GetMicrosecondCount ( ) : 0 ;
2026-07-14 22:36:27 -04:00
Expert . OnTick ( ) ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
ulong tp2 = g_tpActive ? GetMicrosecondCount ( ) : 0 ;
fix: four risk-layer holes a funded account would eventually find
1. The expectancy stop was stone dead at shipped defaults. Its only feed -
RecordTradeResult inside CTradeJournalManager::Update() - ran solely under
UseDatabaseRanking, which ships false, so the da54639 halt was armed
(ExpectancyMinTrades=40) and never received a single closed trade. A risk
rule must not be a side effect of an analytics toggle: the journal gains
InitTrackingOnly(), Update() runs unconditionally from OnTick and skips
only the DB insert when no DB was initialized.
2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by
TCNormalizeVolume - correct for a user-entered fixed lot, but in the
risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/
step-0.01 symbols: double the intended risk, after CapRiskAmount already
clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario
the budget exists to close. CMoneyRiskBase now refuses the trade when the
risk-derived lot is below the broker minimum.
3. All trading was async fire-and-forget (SetAsyncMode(true)) with no
OnTradeTransaction handler and no retry: server retcodes were never
observed. Fail-safe for entries, not for closes - a silently rejected
close rode the position until the next bar (or next day for the timed
close window). Now synchronous, matching the risk-budget flatten's own
already-synchronous CTrade; on an H1 EA the latency is irrelevant.
4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no
OpenRiskAtStops) - pre-halt it could commit more than the remaining daily
allowance. A fixed lot cannot be scaled, so the rule is binary: its
loss-to-stop fits the remaining allowance whole or the trade is refused;
unpriceable risk (no SL) is refused while the budget is enabled.
Compile: 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00
//--- Unconditional since 2026-08-11: Update() feeds the expectancy stop from every closed trade
//--- and only touches the journal DB when one was initialized (UseDatabaseRanking).
journal . Update ( ) ;
2026-07-14 22:36:27 -04:00
//--- new arrows are always created visible; if signals are currently hidden, re-hide
//--- any that were drawn this tick (cheap - only runs while the toggle is in the "hidden" state)
if ( ! g_signalsVisible )
ApplySignalsVisibility ( ) ;
perf(tester,bn): no sub-second timer in the tester + BN kernels on the DLL tier
THE OPTIMIZER ("0.1% an hour per agent", 0 of 39 passes in 78 min,
12 agents): the tester fires OnTimer on SIMULATED time, so the live
chart's 500ms EventSetMillisecondTimer over a 2016-2026 pass is ~600
MILLION OnTimer calls - each walking 4x PollTraining, the vote
readout's string build, the overlay advance and the deployed census.
None of it serves an inference-only pass: training never runs, per-bar
inference is driven by OnTickHandler off the tick stream, the risk
budget re-checks in OnTick, and there is no chart to keep fresh.
StepSetTimer now arms EventSetTimer(3600) in tester/optimizer/forward
(~2,600 calls per pass) and keeps the 500ms timer for live charts.
Plus a TESTER PASS SELF-PROFILE: per-tick buckets (pre / Expert.OnTick
/ journal) and the timer total, printed once at the pass's OnDeinit -
so if a pass is still slow it names its own consumer instead of being
diagnosed from outside.
OFFLOAD (operator: "as much calculation as possible to DLL/OpenCL"):
batch norm was the ONE stage still host-side on the DLL tier - the
device path was OpenCL-only, so every sample crossed the bus twice per
BN layer and normalized in interpreted MQL5 (and every model runs
batchnorm ON). Four new exports mirror AI\Network.cl's BatchNorm*
kernels 1:1 in DOUBLE precision (closer to the host reference than
the float OpenCL kernels): forward with running stats + frozen flag,
hidden gradient with the clamp derivative, gamma/beta accumulate, and
the batch-mean apply (no weight decay, moments-before-skip ordering,
sqrt-stored v). BnDeviceEligible/EnsureBnDeviceBuffers/all four
Dispatch* now route by backend; the EXISTING in-situ self-checks
(host-vs-device on the first real sample, latch-off + host fallback on
mismatch) verify the DLL kernels exactly as they verified OpenCL ones.
batch_accum_check regression: ALL CHECKS PASSED on the rebuilt DLL.
Same deployment coupling as bd46374: the .ex5 imports the new exports
- copy DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed)
together with the new .ex5, and re-copy it to the tester agents (or
just run DirectML\build_cpu.bat once with everything closed - it
deploys to every discovered Libraries folder).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:44:18 -04:00
if ( g_tpActive )
{
g_tpTicks + + ;
g_tpPreUs + = tp1 - tp0 ;
g_tpExpertUs + = tp2 - tp1 ;
g_tpJournalUs + = GetMicrosecondCount ( ) - tp2 ;
}
2026-07-14 22:36:27 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void OnChartEvent ( const int id ,
const long & lparam ,
const double & dparam ,
const string & sparam )
{
2026-08-22 00:25:52 -04:00
//--- STOP FIRST, and this handler matters more than the other two: training is driven by a CUSTOM
//--- CHART EVENT (see CExpertSignalAIBase::OnChartEventHandler -> TuneIndicatorsAndTrain), so an
//--- event already queued when the stop request lands would start a full era-0 warm-up - the MI
//--- suite, the geometry scan, a relabel - inside the teardown window.
fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks
Two things, one of which was a compile error.
1. ExitPolicy() was declared in the protected block but is pushed in from
Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters.
2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured
from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot
begin until whatever is in flight returns - so a scan still running after
_StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge
never gets its turn.
New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress.
Deliberately NOT m_trainingStopRequested: that latches, and a latched flag
would permanently disable scans that must run again on the next Start.
Guarded, longest first:
- TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings
on the way out (best[] is mutated in place; the tuner otherwise keeps the
last trial's parameters, which nothing chose).
- ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so
m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar
read the last candidate's multiples as the configured geometry).
- ReportFeatureLabelInformation / ReportExcursionInformation / lag profile -
nulls ABANDON rather than truncate: fewer draws is not a smaller null, it
is a wrong one, and p shifts toward significance. m_dirEvidence staying
false is the safe direction.
- SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line
is dropped instead of latching a partial expectancy as the run's only report.
- ReportGeometryExpectancyScan - per ladder rung.
- HttpGet - one choke point for up to a dozen blocking WebRequests per
first-pass Update(). An in-flight request cannot be cancelled; refusing to
start another is the whole remedy.
- PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain -
entry points, so a queued event cannot open an era during teardown.
TuneIndicatorsAndTrain's guard is the first statement, ahead of the
m_tuneFilterDone / g_ensembleChartTuneDone latches.
- OnTick / OnTimer / OnChartEvent.
Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3
yield on a 120 ms budget); the warm-up scans did not, and they are the longest
uninterruptible stretches the EA has.
StopTraining() is unchanged: the operator's Stop still finalises synchronously.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
if ( IsStopped ( ) )
return ;
2026-07-14 22:36:27 -04:00
//--- canonical CAppDialog usage (Controls\Dialog.mqh): forward every event to the dialog first, since
//--- that's what drives its own click/drag hit-testing (via CHARTEVENT_MOUSE_MOVE) as well as our
//--- buttons' EVENT_MAP handlers (see ControlPanel.mqh) - then pick up whatever button action, if any,
//--- that just recorded.
ExtPanel . ChartEvent ( id , lparam , dparam , sparam ) ;
HandleControlPanelAction ( ExtPanel . ConsumeAction ( ) ) ;
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//--- The alt-data mapping dialog, when open, drives its own hit-testing the same way.
if ( g_altMapDialogOpen )
{
g_altMapDialog . ChartEvent ( id , lparam , dparam , sparam ) ;
PollAltDataMapDialog ( ) ;
}
2026-07-14 22:36:27 -04:00
Expert . OnChartEvent ( id , lparam , dparam , sparam ) ;
2026-07-26 14:45:08 -04:00
//--- CHARTEVENT_CHART_CHANGE covers resize, scroll and DPI/zoom changes - anything that can
//--- move the visible area out from under a dialog left near an edge from a previous drag.
if ( id = = CHARTEVENT_CHART_CHANGE )
ClampControlPanelToChart ( ) ;
2026-07-14 22:36:27 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool AddFilterToSignal ( CExpertSignalCustom * signal , CExpertSignalCustom * filter )
{
if ( filter = = NULL )
{
Print ( __FUNCTION__ + " Error creating filters " ) ;
return false ;
}
return signal . AddFilter ( filter ) ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool InitializeSignal ( CExpertSignalCustom * signal_obj )
{
if ( signal_obj = = NULL )
{
Print ( __FUNCTION__ + " : error creating signal " ) ;
return false ;
}
Expert . InitSignal ( signal_obj ) ;
signal_obj . Entry_Multiplier ( Entry_Multiplier ) ;
signal_obj . Expiration ( Signal_Expiration ) ;
2026-08-11 21:53:37 -04:00
//--- ATR unit lookback, pinned - decoupled from the derived input window, see Inputs.mqh.
signal_obj . Periods ( ATR_FEATURE_PERIOD ) ;
2026-07-22 13:33:56 -04:00
signal_obj . SLMode ( ( int ) SL_Mode ) ;
signal_obj . TPMode ( ( int ) TP_Mode ) ;
2026-07-22 22:51:04 -04:00
//--- Gates AddFilter()'s DB pattern-table creation and Direction()'s per-tick DB signal buffering
//--- (ExpertSignalCustom.mqh:286/555) - without this, UseDatabaseRanking only skipped the Weight(1)
//--- default below and never actually populated the win-rate tables UpdateSignalsWeights() reads from.
signal_obj . UseDatabase ( UseDatabaseRanking ) ;
2026-08-12 15:20:33 -04:00
//--- Pattern-table row cap; raised via the input for meta-label corpus builds (design doc S1).
signal_obj . MaxTableRows ( DB_MaxRowsPerTable ) ;
feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00
//--- THE SEED ONLY. The real open threshold is derived per era and pushed in by
//--- CExpertCustom::PublishVoteThreshold(); this is what trades until the first era is scored.
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_obj . ThresholdOpen ( ( int ) Signal_ThresholdOpen ) ;
feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.
It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.
Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.
THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.
THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.
Retrain-neutral. Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
//--- THE EXIT, AS ONE BOOLEAN. OFF pins the close threshold to an arithmetically unreachable 101
//--- (the stock default of 100 IS reachable by a weighted mean of values capped at 100, which is
//--- why this is set explicitly rather than left alone). ON pins it to the SAME threshold the
//--- entry uses - the seed here, then the derived value from PublishVoteThreshold() once an era
//--- has been scored. Either way there is no second number to tune.
signal_obj . ThresholdClose ( Exit_On_Reversal_Vote ? ( int ) Signal_ThresholdOpen
: VOTE_EXIT_DISABLED_THRESHOLD ) ;
//--- The dormant half of the same policy, finally armed. m_holdToBarrier short-circuits
//--- CheckClosePosition() before it ever looks at a threshold, and until now NOTHING SET IT - the
//--- disabled threshold was carrying the whole policy by itself.
signal_obj . HoldToBarrier ( ! Exit_On_Reversal_Vote ) ;
2026-08-22 00:25:52 -04:00
//--- The hold-to-barrier exit policy went with the fractal target (withdrawn - see Inputs.mqh).
//--- Barrier-target models keep the vote exits and always did: their label IS the vote's own
//--- horizon.
2026-07-27 22:08:55 -04:00
//--- HYBRID is now one fused signal, so no separate AI quorum is needed here.
2026-07-14 22:36:27 -04:00
return true ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
// Initialize Trailing
bool InitializeTrailing ( )
{
2026-08-24 02:36:32 -04:00
switch ( TrailingStrategy )
2026-07-14 22:36:27 -04:00
{
2026-08-24 02:36:32 -04:00
case TRAILING_STRATEGY_NONE :
// No trailing strategy selected
return true ;
case TRAILING_STRATEGY_ATR_x1 :
case TRAILING_STRATEGY_ATR_x2 :
case TRAILING_STRATEGY_ATR_x3 :
{
// ATR Trailing Strategy
double multiplier = 0 ;
switch ( TrailingStrategy )
2026-07-14 22:36:27 -04:00
{
2026-08-24 02:36:32 -04:00
case TRAILING_STRATEGY_ATR_x1 :
2026-07-14 22:36:27 -04:00
multiplier = 1 ;
2026-08-24 02:36:32 -04:00
break ;
case TRAILING_STRATEGY_ATR_x2 :
multiplier = 2 ;
break ;
case TRAILING_STRATEGY_ATR_x3 :
multiplier = 3 ;
break ;
2026-07-14 22:36:27 -04:00
}
2026-08-24 02:36:32 -04:00
CTrailingATR * trailing = new CTrailingATR ;
if ( trailing = = NULL )
{
Print ( __FUNCTION__ + " : error creating trailing " ) ;
return false ;
}
// Set ATR Multiplier
trailing . Multiplier ( multiplier ) ;
if ( ! Expert . InitTrailing ( trailing ) )
{
Print ( __FUNCTION__ + " : error initializing trailing " ) ;
return false ;
}
break ;
}
}
2026-07-14 22:36:27 -04:00
// Add more trailing strategies if needed
return true ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool InitializeMoneyManagement ( )
{
string functionName = __FUNCTION__ ;
2026-08-24 02:36:32 -04:00
switch ( MM_STRATEGY )
2026-07-14 22:36:27 -04:00
{
2026-08-24 02:36:32 -04:00
case FIXED_RISK :
{
CMoneyFixedRisk * money = CreateAndInitMoney < CMoneyFixedRisk > ( functionName ) ;
if ( money = = NULL )
return false ;
money . Percent ( Money_Risk_Percent ) ;
break ;
}
case FIXED_LOT :
2026-07-14 22:36:27 -04:00
{
2026-08-24 02:07:33 -04:00
CMoneyFixedLot * money = CreateAndInitMoney < CMoneyFixedLot > ( functionName ) ;
2026-07-14 22:36:27 -04:00
if ( money = = NULL )
return false ;
money . Lots ( Money_FixLot_Lots ) ;
2026-08-24 02:36:32 -04:00
break ;
2026-07-14 22:36:27 -04:00
}
2026-08-24 02:36:32 -04:00
}
2026-07-14 22:36:27 -04:00
// Add more money management strategies if needed
return true ;
}
//+------------------------------------------------------------------+