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-07-22 17:17:23 -04:00
//--- MQL5 Market rule IV: "Products must not contain calls to any DLL". Uncomment the next line before
//--- compiling the Market-submission .ex5 - it compiles the WarriorDML.dll/WarriorCPU.dll #import blocks
//--- and every call into them out of AI\NeuronDirectML.mqh entirely (see that file), leaving OpenCL then
//--- plain-MQL5 CPU (AI\NeuronCPU.mqh) as the only compute tiers - zero DLL calls in the resulting
//--- binary. Leave commented out for the private/prop-firm deployment build, which keeps the DirectML/
//--- CPU-DLL fallback tier for hosts without OpenCL.
//#define WARRIOR_MARKET_BUILD
2026-07-23 15:28:04 -04:00
//--- MARKET build only: embed every custom indicator this EA calls via CiCustom so one self-contained
//--- .ex5 ships to the Market. Paths embed straight from the terminal's standard MQL5\Indicators\ folder
//--- (leading backslash = MQL5 root) - the SAME place the indicators already live for the private build's
//--- runtime load, so you maintain ONE copy. The indicators must be compiled x64/non-AVX (CLI compile, or
//--- disable AVX in the compiler options) or the embed fails with error 414. IMPORTANT: the leading-
//--- backslash root only resolves when the compiler knows the MQL5 tree - build the Market .ex5 from
//--- INSIDE the terminal (MQL5\Experts\...) or via the MetaEditor CLI with an include path. Compiling
//--- from an external folder in the GUI makes the root fall back to the MQL5\Files\ sandbox and the embed
//--- can't find the files. The private build uses bare names (no resources), so it builds from anywhere.
//--- The "::" reference paths are built by WARRIOR_CI() in Variables\IndicatorResources.mqh - keep in sync.
# ifdef WARRIOR_MARKET_BUILD
# resource "\\Indicators\\ADCumulativeDelta.ex5"
# resource "\\Indicators\\ADShorteningOfThrust.ex5"
# resource "\\Indicators\\ADWyckoffEventStream.ex5"
# resource "\\Indicators\\ADWyckoffFailedStructure.ex5"
# resource "\\Indicators\\ADWyckoffSignificantBarInversion.ex5"
# resource "\\Indicators\\ADZigZag.ex5"
# resource "\\Indicators\\ADMovingAverage.ex5"
# endif
2026-07-22 17:17:23 -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. The AI\Network.mqh optimizer/CPU inputs are declared in that header (parsed later)
//--- and so render at the END, under Inputs.mqh's "NN Optimizer / Performance" divider.
# include "Variables\Inputs.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"
2026-08-12 15:20:33 -04:00
# include "Expert\AIBase\MetaCorpus.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-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: add unified MA type support to indicator tuner
Add `MA_TYPE_PRESETS` enum covering advanced (ALMA, DEMA, ZLEMA, T3, Kalman) and standard (SMA, EMA, SMMA, LWMA) moving averages. Integrate `maType` and `bestMaType` into `CADIndicatorTuner` struct, update flatten/unflatten routines, and bump `AD_TUNE_PARAM_COUNT` to 33. This allows the auto-tuner to search over MA type alongside period, improving feature discovery.
2026-07-23 15:02:09 -04:00
//| The CustomIndicators\*.mq5 files (ADCumulativeDelta, |
2026-07-14 22:36:27 -04:00
//| ADShorteningOfThrust, ADWyckoffEventStream, |
feat: add unified MA type support to indicator tuner
Add `MA_TYPE_PRESETS` enum covering advanced (ALMA, DEMA, ZLEMA, T3, Kalman) and standard (SMA, EMA, SMMA, LWMA) moving averages. Integrate `maType` and `bestMaType` into `CADIndicatorTuner` struct, update flatten/unflatten routines, and bump `AD_TUNE_PARAM_COUNT` to 33. This allows the auto-tuner to search over MA type alongside period, improving feature discovery.
2026-07-23 15:02:09 -04:00
//| ADWyckoffFailedStructure, ADWyckoffSignificantBarInversion, |
//| ADZigZag, and the unified ADMovingAverage) are loaded via |
//| CiCustom/IND_CUSTOM (see ExpertSignalAIBase.mqh / SignalMA.mqh). |
//| Name resolution is build-conditional - see |
//| Variables\IndicatorResources.mqh (WARRIOR_CI): |
//| - MARKET build (WARRIOR_MARKET_BUILD): each indicator is EMBEDDED |
2026-07-23 15:28:04 -04:00
//| as a #resource (block above) from the standard MQL5\Indicators\ |
//| folder and called directly via its "::" resource path, so one |
//| self-contained .ex5 ships to the Market with no external files. |
//| A resource indicator is NOT extracted to disk. Same single copy |
//| the private build already loads at runtime - compile x64/non-AVX |
//| (CLI, or disable AVX) or the embed fails w/ err 414, and build |
//| the Market .ex5 from inside the terminal tree (or CLI /inc) so |
//| the leading-backslash MQL5 root resolves (an external GUI compile|
//| falls back to the MQL5\Files\ sandbox and can't find them). |
feat: add unified MA type support to indicator tuner
Add `MA_TYPE_PRESETS` enum covering advanced (ALMA, DEMA, ZLEMA, T3, Kalman) and standard (SMA, EMA, SMMA, LWMA) moving averages. Integrate `maType` and `bestMaType` into `CADIndicatorTuner` struct, update flatten/unflatten routines, and bump `AD_TUNE_PARAM_COUNT` to 33. This allows the auto-tuner to search over MA type alongside period, improving feature discovery.
2026-07-23 15:02:09 -04:00
//| - Private build: referenced by bare name and loaded from |
//| <MQL5>\Indicators\ at call time (faster than unpacking a |
//| resource; dev terminals already have them deployed). Init*() |
//| fails CiCustom::Create() with a clear log line if a matching |
//| .ex5 isn't present. |
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 |
2026-07-27 22:08:55 -04:00
//| created (per AIType - MLP/CONV/LSTM/HYBRID), so the control |
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. |
//+------------------------------------------------------------------+
# define MAX_AI_SIGNALS 3
2026-07-27 15:52:39 -04:00
//--- Printed at OnInit so tester logs prove which binary is actually running.
perf: pass 1 forward-passed ~40% of bars that a later pass redid anyway
Pass 1 already skipped its feedForward on QUEUED bars, because pass 2 redoes
them. The same argument covers two more bands it was still forwarding:
OOS window (30% of bars) - pass 3 re-forwards every one of them
calibration band (~10% of bars) - pass 2.5 re-forwards every one of them
All three passes derive their bounds from the same helpers and apply the
identical eligibility test, so the bar sets are equal by construction, not by
coincidence. Only the two purge bands and the ineligible edge bars are visited
in pass 1 and nowhere else - those keep their forward pass.
The scan's copy was never the one that survived. Its arrow-cache write was
overwritten by pass 3's (with the thresholded, post-training decision), its
status-label paint was transient, and its predicted-class tally measured
last era's weights. Those tallies move to pass 2.5 and pass 3, on the raw
argmax exactly as pass 1 and pass 2 count it, so the population behind the
panel's "Predicted -> Buy/Sell/Neutral" line is unchanged and stays comparable
with the "Actual" line beside it, which pass 1 still accumulates over every
labelled bar.
Verified unaffected by the cut: dPrevSignal and m_lastBarTime are both written
last by bars 0/1, which are label-ineligible and therefore still forwarded, so
FinalizeTrainRun's `dtStudied = m_lastBarTime` and Lifecycle's newBarPending
sentinel read the same values as before.
Correctness, not just speed: batch norm is UNFROZEN during pass 1 (passes 2.5
and 3 freeze it deliberately), so every scan-time forward on a held-out bar was
advancing the BN running mean/variance from data the model is graded on. Those
running statistics are inference-time model state. It is the mild,
unsupervised kind of leakage - feature statistics, not labels - but it fed the
weights pass 3 then scored, and it is now gone.
Cost: ~40% of all bars lose one forward pass per era, ~16% of net time once
pass 2's backward pass is weighted in. Per-dispatch, so it lands on every
backend.
Both variants compile 0 errors / 0 warnings. Build tag scan-nofwd-v5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:50:14 -04:00
# define WARRIOR_BUILD_TAG " scan-nofwd-v5 "
2026-07-14 22:36:27 -04:00
CExpertSignalAIBase * g_aiSignals [ MAX_AI_SIGNALS ] ;
int g_aiSignalCount = 0 ;
void RegisterAISignal ( CExpertSignalAIBase * sig )
{
if ( sig = = NULL | | g_aiSignalCount > = MAX_AI_SIGNALS )
return ;
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 ;
bool g_signalsVisible = true ;
2026-07-27 11:13:19 -04:00
# define SIGNAL_VISIBILITY_STATE_SUFFIX " .sigvis "
string SignalsVisibilityStateFile ( void )
{
return eaName + " _ " + Symbol ( ) + " _ " + IntegerToString ( Period ( ) ) + SIGNAL_VISIBILITY_STATE_SUFFIX ;
}
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
//--- would otherwise only be as fresh as the last fully-completed training era. A periodic
//--- autosave closes that gap without depending on UseDatabaseRanking's timer.
2026-07-26 10:27:38 -04:00
//--- Fires on NEW BAR CLOSE, not a fixed wall-clock interval (a prior 300s timer saved up to ~12x more
//--- often than an H1 chart ever has new state to persist - nothing changes between bar closes, since
//--- both training/era-end saves and OnlineLearnStep's continual-learning updates are themselves bar-
//--- driven). Every save atomically renames the SAME shared FILE_COMMON model file a Strategy Tester
//--- backtest may be concurrently reading via FileCopy (see CopyFileWithRetry's declaration comment) -
//--- cutting write frequency to the real update cadence directly shrinks that collision window instead
//--- of just papering over it with more retries.
datetime g_lastAutosaveBarTime = 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
//--- (e.g. AIType=All with one signal paused and one running) still shows an actionable label
bool AllTrainingPaused ( void )
{
if ( g_aiSignalCount = = 0 )
return false ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( ! g_aiSignals [ i ] . IsTrainingPaused ( ) )
return false ;
return true ;
}
bool AllTrainingStopped ( void )
{
if ( g_aiSignalCount = = 0 )
return false ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( ! g_aiSignals [ i ] . IsTrainingStopped ( ) )
return false ;
return true ;
}
2026-07-25 16:39:11 -04:00
//--- "deployed" = every active signal has finalised a model and is running live inference rather than
//--- training. This is the state that makes Pause/Stop meaningless (there is no run to pause or stop),
//--- so it drives BOTH the Deploy button's own label and the n/a labels on those two - see
//--- RefreshControlPanelLabels().
bool AllTrainingDeployed ( void )
{
if ( g_aiSignalCount = = 0 )
return false ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( ! g_aiSignals [ i ] . TrainingComplete ( ) )
return false ;
return true ;
}
//--- 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 )
{
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( ! g_aiSignals [ i ] . TrainingComplete ( ) & & ! g_aiSignals [ i ] . HasRecallPassingCheckpoint ( ) )
return true ;
return false ;
}
2026-07-14 22:36:27 -04:00
void ApplySignalsVisibility ( void )
{
for ( int i = ObjectsTotal ( 0 , 0 , OBJ_ARROW ) - 1 ; i > = 0 ; i - - )
{
string name = ObjectName ( 0 , i , 0 , OBJ_ARROW ) ;
ObjectSetInteger ( 0 , name , OBJPROP_TIMEFRAMES , g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS ) ;
}
ChartRedraw ( 0 ) ;
}
void ToggleSignalsVisibility ( void )
{
g_signalsVisible = ! g_signalsVisible ;
2026-07-26 12:36:56 -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
2026-07-26 12:52:56 -04:00
//--- CExpertSignalAIBase::StartChartSignalRescan/AdvanceChartSignalRescan). Skipped on Hide - nothing
//--- to refresh when hiding, so that path stays instant.
//--- Rescans are QUEUED here, not run to completion - each is a real per-bar inference pass over up to
//--- SIGNAL_RESCAN_LOOKBACK_BARS bars, chunked across PollTraining's timer slices so the click handler
//--- never blocks. Visibility is applied and the "shown" Alert fires later, once
//--- FinalizeSignalsRescanIfDone() sees every instance's RescanPending() clear (called from OnTimer).
2026-07-26 12:36:56 -04:00
if ( g_signalsVisible )
2026-07-26 12:52:56 -04:00
{
bool anyQueued = false ;
2026-07-26 12:36:56 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
2026-07-26 12:52:56 -04:00
if ( g_aiSignals [ i ] . StartChartSignalRescan ( ) )
anyQueued = true ;
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 ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( g_aiSignals [ i ] . RescanPending ( ) )
return ; // at least one instance still scanning - check again next tick
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 " ) ;
bool noAI = ( g_aiSignalCount = = 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.
//--- Deployed is the state that matters most: with a finalised model there is no run left to pause or
//--- stop, and the only meaningful move is to put it back into training (which is what Deploy toggles
//--- to). See CExpertSignalAIBase::DeployNow/RetrainDeployed.
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
//--- (create in OnInit, destroy in OnDeinit; see Controls\Dialog.mqh). CAppDialog::Destroy(REASON_PROGRAM)
//--- calls ExpertRemove() once the dialog has a valid PROGRAM_EXPERT type (i.e. on any call after the
//--- first successful Create()), so it must never be called speculatively/defensively before Create() -
//--- doing so would silently detach this EA from the chart the next time this function ran.
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 ;
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
//--- 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. A deinit force-terminated at MetaTrader's ~4,500 ms budget strands
//--- the whole panel, and the next attach then draws a SECOND one on top of the corpse - the reported
//--- "stale copy of the panel" that survived deleting every file the EA owns. Deleting by prefix before
//--- Create() is idempotent (normally removes nothing) and makes the panel single by construction.
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-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 )
{
case CP_ACTION_TOGGLE_SIGNALS :
ToggleSignalsVisibility ( ) ;
2026-07-26 12:52:56 -04:00
//--- 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. Hide, or a Show with nothing to rescan,
//--- completes synchronously so it's safe to report right away.
if ( ! g_signalsRescanPending )
{
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: signal arrows " + ( g_signalsVisible ? " shown " : " hidden " ) ) ;
}
2026-07-14 22:36:27 -04:00
break ;
case CP_ACTION_TOGGLE_PAUSE :
{
2026-07-25 16:39:11 -04:00
//--- 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. " ) ;
break ;
}
2026-07-14 22:36:27 -04:00
bool pause = ! AllTrainingPaused ( ) ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( pause )
g_aiSignals [ i ] . PauseTraining ( ) ;
else
g_aiSignals [ i ] . ResumeTraining ( ) ;
RefreshControlPanelLabels ( ) ;
2026-07-22 22:51:04 -04:00
Alert ( " Warrior EA: training " + ( pause ? " paused " : " resumed " ) ) ;
2026-07-14 22:36:27 -04:00
break ;
}
case CP_ACTION_TOGGLE_STOP :
{
2026-07-25 16:39:11 -04:00
//--- 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. " ) ;
break ;
}
2026-07-14 22:36:27 -04:00
bool doStop = ! AllTrainingStopped ( ) ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( doStop )
g_aiSignals [ i ] . StopTraining ( ) ;
else
g_aiSignals [ i ] . StartTraining ( ) ;
RefreshControlPanelLabels ( ) ;
2026-07-22 22:51:04 -04:00
Alert ( " Warrior EA: training " + ( doStop ? " stopped " : " restarted " ) ) ;
2026-07-14 22:36:27 -04:00
break ;
}
2026-07-25 16:39:11 -04:00
case CP_ACTION_TOGGLE_DEPLOY :
{
if ( g_aiSignalCount = = 0 )
{
Alert ( " Warrior EA: no AI signal is active - nothing to deploy (set the AI algorithm input to something other than Disabled). " ) ;
break ;
}
//--- 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 ( ) )
{
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . RetrainDeployed ( ) ;
RefreshControlPanelLabels ( ) ;
Alert ( " Warrior EA: retraining the deployed model - it continues from its current weights. \n Use \" Delete & Reset Weights \" instead to start from scratch. " ) ;
break ;
}
//--- 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 " ) ;
break ;
}
int deployed = 0 ;
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
if ( g_aiSignals [ i ] . DeployNow ( ) )
deployed + + ;
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. " ) ;
break ;
}
2026-07-14 22:36:27 -04:00
case CP_ACTION_SAVE :
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . SaveWeightsNow ( ) ;
2026-07-22 22:51:04 -04:00
Alert ( " Warrior EA: weights saved " ) ;
2026-07-14 22:36:27 -04:00
break ;
case CP_ACTION_LOAD :
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . LoadWeightsNow ( ) ;
2026-07-25 16:39:11 -04:00
//--- 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 ( ) ;
2026-07-22 22:51:04 -04:00
Alert ( " Warrior EA: weights reloaded from disk " ) ;
2026-07-14 22:36:27 -04:00
break ;
case CP_ACTION_RESET :
2026-07-23 08:48:44 -04:00
if ( ! ConfirmDestructiveAction ( " Delete the saved AI weights and restart training from era 0? " ) )
{
Alert ( " Warrior EA: weights reset cancelled " ) ;
break ;
}
2026-07-14 22:36:27 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
g_aiSignals [ i ] . ResetWeights ( ) ;
RefreshControlPanelLabels ( ) ;
2026-07-22 22:51:04 -04:00
Alert ( " Warrior EA: weights reset - training restarts from era 0 " ) ;
break ;
case CP_ACTION_REPORT :
{
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 " ) ;
break ;
}
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 ) ;
}
break ;
}
//--- separate from CP_ACTION_RESET 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 now.
case CP_ACTION_RESET_DB :
if ( UseDatabaseRanking )
{
2026-07-23 08:48:44 -04:00
if ( ! ConfirmDestructiveAction ( " Delete the trade-journal and pattern-confidence database? " ) )
{
Alert ( " Warrior EA: database reset cancelled " ) ;
break ;
}
2026-07-22 22:51:04 -04:00
dbm . ResetDatabase ( ) ;
Alert ( " Warrior EA: database reset " ) ;
}
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
break ;
default :
break ;
}
}
// Helper function to pause execution for a random duration between 1 to 3 seconds
void RandomSleep ( )
{
Sleep ( MathRand ( ) % 2000 + 1000 ) ; // Sleeps between 1000ms (1s) and 3000ms (3s)
}
2026-07-17 23:21:12 -04:00
//+------------------------------------------------------------------+
2026-07-22 22:51:04 -04:00
//| Config fingerprint appended to the pattern-confidence/trade- |
//| journal database's filename, so a topology or feature-set change |
//| 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. Same |
//| FNV-1a scheme CExpertSignalAIBase uses for its own .nnw cache-key |
//| fingerprint (Expert\ExpertSignalAIBase.mqh), computed here from |
//| the raw inputs directly since the DB opens before any AI signal |
//| object exists to ask. |
//+------------------------------------------------------------------+
string ComputeDbConfigFingerprint ( )
{
refactor(ai): derive the first dense layer's width instead of asking for it
InitialNeurons was an input whose only defensible value depends on two
things the user cannot see when picking from a dropdown: how wide the input
vector ended up after feature selection, and how much in-sample data the
study period actually yields. Left to a hand-picked constant it was badly
wrong - 500 units against a 420-wide input is 210,500 weights, 72% of a
292,583-weight model, against ~36,500 training bars of which only ~2,236
are directional. That is 6.6 weights per training bar, and it EXPANDS a set
of highly correlated inputs rather than compressing them.
The symptom was already in the logs and had been read as a depth problem:
the shallowest topology consistently beat the deepest (perceptron 52.7%
balanced, hybrid 41.3%). Over-parameterization predicts that ordering just
as well as covariate shift does, and only one of the two had been addressed.
ComputeFirstLayerWidth() budgets roughly one first-layer weight per
in-sample bar. Measured across the configurations in use:
M15 10y -> 256 units, 129,071 weights, 0.73 per bar
H1 10y -> 64 units, 28,727 weights, 0.65 per bar
H4 10y -> 16 units, 7,559 weights, 0.68 per bar
Two design points that matter:
- It estimates in-sample bars from the STUDY PERIOD and timeframe, not
from Bars(). What is downloaded grows over a terminal's lifetime, and a
topology that widened as history filled in would re-key its own weights
file and discard a trained model.
- The result is snapped down to a coarse power-of-two ladder, so the
estimate would have to be wrong by ~2x to change the answer.
Every field it reads is already part of the weights-filename fingerprint,
so the derived value needs no fingerprint entry of its own. The public
setter is removed - it could only have been called after construction, and
would either be ignored or silently re-key the model mid-run.
Where the data cannot support even the floor (D1 over 10 years is under
2,000 bars) it now says so and names the fixes, rather than quietly
training a model with more weights than examples.
The DB config fingerprint drops the term too, which re-keys existing
pattern databases once - correct, since a model an order of magnitude
smaller should not inherit the old one's win-rate history.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 13:01:16 -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. Dropping the term re-keys
//--- existing databases once - which is correct, because a model an order of magnitude smaller
//--- genuinely is a different config and should not inherit the old one's pattern win-rate history.
2026-07-30 09:22:11 -04:00
//--- LstmHiddenSize and ConvFilterCount left for the same reason 2026-07-30 (see
//--- ComputeLstmHiddenSize/ComputeConvFilterCount) - both are now functions of the feature flags and
feat(nn): derive dense depth, train on all history, pin the shape in .cfg
Completes the derived-topology work. Three inputs removed.
AIType loses its depth suffix - AI_MLP/AI_CONV/AI_LSTM/AI_HYBRID, five
entries instead of eight. Depth is now derived from the two endpoints
the taper already has to connect (derived first-layer width, output-tied
final width) at a 2x per-layer compression target, clamped [2..5].
Asking a user to pick a layer count while the code derives the widths
those layers taper between was asking for half a decision: at 64 units
tapering to 12, four layers compress by 1.4x per step and five by 1.3x,
so the extra depth bought no abstraction. On the shipping H1/10y default
the derivation lands on 3 layers - the depth that actually won Run 2.
StudyPeriods removed. There is no case for training on less data than
the broker provides at a ~6% directional base rate; the honest
generalization read comes from the OOS holdout, not from withholding
history. Training now starts at the earliest available bar, floored by
MinTrainYear, which answers a different question (excluding dubious
pre-history) and stays.
That required closing the hazard the old code documented: the capacity
budget now MEASURES the symbol's real bar count, and a topology derived
from a measurement would widen as history downloads. Both ends are now
pinned. Every derived value left the weights-filename fingerprint -
keying a filename on a measured quantity means the EA looks for a file
that does not exist, starts from era 0 and orphans a trained model,
silently, because a missing cache is the normal first-run state. The
shape lives in the .cfg instead, where LoadAndCompare now ADOPTS the
four derived fields rather than diffing them; a mismatch there would
discard a fully-trained model over nothing the user did. Two fields
appended to the .cfg for the conv/LSTM stages, length-guarded on read
because FileReadInteger past EOF returns 0 with no error.
ForceHiddenLayers, a compile-time constant like DebuggingMode, pins
depth for diagnostic comparisons. It joins the fingerprint only when
non-zero, so forced depths get their own files - sequential comparisons
only, not simultaneous from one .ex5.
Derived shape, H1/10y defaults (21 features x 20 bars): first layer 64,
3 dense, 8 conv filters, 16 LSTM units. The LSTM block halves from
~58k to ~28k weights.
Both builds compile 0 errors, 0 warnings. Re-keys existing models.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:05:40 -04:00
//--- ind_Periods already hashed below, so keeping them would hash the same choices twice.
//--- StudyPeriods left because the input itself is gone: training now covers all available history
//--- (see Train()'s window), so there is no longer a user choice here to key a database on.
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
//--- Journaling-semantics term, deliberately UNCONDITIONAL (unlike the enabled-only blocks below):
//--- it versions what a Pattern_N row MEANS, which no input hash can see - the January-August 2026
//--- database blended rows from three different Ichimoku/MA pattern definitions under one key
//--- because only inputs were fingerprinted. Bumping it re-keys every database at once, which is
//--- the point; see the constant's declaration comment in Variables\Variables.mqh for when to bump.
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 " ,
refactor(ai): derive the dense taper's shape, not just its first layer
Deriving the first layer's width left NeuronsReduction and MinNeuronsCount
behind as inputs calibrated for something that no longer exists. Against a
hand-picked 500-wide first layer "keep 30%, floor at 20" produced a genuine
funnel - 500 -> 150 -> 45. Against the derived 64 it degenerates to
64 -> 20 -> 20: the reduction factor stops mattering after one step, and
"minimum neurons per layer" silently becomes the width of every layer but
the first. Two knobs whose labels no longer describe what they do.
The taper now runs geometrically from the derived first-layer width down to
a final hidden layer sized off the output count, spread evenly over however
many layers the chosen AIType implies:
MLP_3L 64 -> 28 -> 12 -> 3 29,151 dense weights
MLP_4L 64 -> 37 -> 21 -> 12 -> 3 30,450
CONV/LSTM/HYBRID_2L 64 -> 12 -> 3 27,763
and it stays a funnel at the floor, where the old rule could not:
D1 (first layer floored to 16) 16 -> 14 -> 12 -> 3
Both inputs are removed. With the width derived there is no freedom left in
the taper, so keeping either would only let the user contradict the
derivation. The layer COUNT stays selectable, because it is bundled into
AIType alongside the conv/LSTM front-end - depth is an architecture choice,
not a data-derived quantity, and pairing them means the two cannot
contradict each other.
m_minNeuronsCount / m_neuronsReduction survive as frozen members: nothing
reads them to build a topology any more, but they hold positional slots in
the .cfg sidecar and the weights fingerprint, and changing either value
would re-key every model on disk for no behavioural reason.
The DB config fingerprint drops both terms.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:03:42 -04:00
( int ) AIType , ( 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 ,
feat: add unified MA type support to indicator tuner
Add `MA_TYPE_PRESETS` enum covering advanced (ALMA, DEMA, ZLEMA, T3, Kalman) and standard (SMA, EMA, SMMA, LWMA) moving averages. Integrate `maType` and `bestMaType` into `CADIndicatorTuner` struct, update flatten/unflatten routines, and bump `AD_TUNE_PARAM_COUNT` to 33. This allows the auto-tuner to search over MA type alongside period, improving feature discovery.
2026-07-23 15:02:09 -04:00
EnableMA , ( int ) PeriodMA , ( int ) MA_Type , EnableRSI , ( int ) PeriodRSI ,
2026-07-22 22:51:04 -04:00
EnableSwingContext , EnableNews ,
EnableADCumulativeDelta , EnableADShorteningOfThrust , EnableADWyckoffEventStream )
2026-07-30 09:22:11 -04:00
+ StringFormat ( " |%d|%d|%d|%d " , EnableADWyckoffFailedStructure , EnableADWyckoffSignificantBarInversion ,
feat: add unified MA type support to indicator tuner
Add `MA_TYPE_PRESETS` enum covering advanced (ALMA, DEMA, ZLEMA, T3, Kalman) and standard (SMA, EMA, SMMA, LWMA) moving averages. Integrate `maType` and `bestMaType` into `CADIndicatorTuner` struct, update flatten/unflatten routines, and bump `AD_TUNE_PARAM_COUNT` to 33. This allows the auto-tuner to search over MA type alongside period, improving feature discovery.
2026-07-23 15:02:09 -04:00
EnableMAFeature , EnableRSIFeature ) ;
2026-07-26 18:33:12 -04:00
//--- MACD/Ichimoku, appended ONLY WHEN ENABLED - same rule, and same reason, as the matching block in
//--- CExpertSignalAIBase::BuildConfigFingerprint(): appending unconditionally would re-key every existing
//--- database the moment this shipped, orphaning the accumulated per-pattern win-rate history of configs
//--- that use neither. A run that enables either one genuinely IS a different config (new voting signals
//--- mean new pattern rows; new input features mean a differently-shaped model) and gets its own DB.
if ( EnableMACD | | EnableMACDFeature )
fp + = StringFormat ( " |MACD:%d:%d:%d:%d:%d " , EnableMACD , EnableMACDFeature ,
( int ) MACD_PeriodFast , ( int ) MACD_PeriodSlow , ( int ) MACD_PeriodSignal ) ;
if ( EnableIchimoku | | EnableIchimokuFeature )
fp + = StringFormat ( " |ICHI:%d:%d:%d:%d:%d " , EnableIchimoku , EnableIchimokuFeature ,
( int ) Ichimoku_PeriodTenkan , ( int ) Ichimoku_PeriodKijun , ( int ) Ichimoku_PeriodSenkou ) ;
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
//--- Cross-asset, conditional for the same reason. Note this ships defaulted ON, so it WILL re-key
//--- every database on first run - which is correct and intended: the input vector genuinely changed
//--- shape, so the accumulated per-pattern history belongs to a different model than the one that
//--- will now train. 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.
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
//+------------------------------------------------------------------+
//| Verifies SL_Mode / TP_Mode actually hold members of their enums. |
//| 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. |
//| |
//| The member lists are spelled out rather than range-checked because |
//| both enums are sparse (TP jumps 4 -> 6 -> 8 -> 10) and carry a |
//| negative sentinel, so no min/max test can distinguish a legal |
//| value from a deleted one - which is the exact case this exists for.|
//+------------------------------------------------------------------+
bool ValidateBarrierInputs ( )
{
int sl = ( int ) SL_Mode ;
bool slOk = ( sl = = SL_INTELLIGENT | | sl = = SL_ATR_x1 | | sl = = SL_ATR_x2 | | sl = = SL_ATR_x3 ) ;
int tp = ( int ) TP_Mode ;
bool tpOk = ( tp = = TP_INTELLIGENT | | tp = = TP_ATR_x1 | | tp = = TP_ATR_x2 | | tp = = TP_ATR_x3 | |
tp = = TP_ATR_x4 | | tp = = TP_ATR_x6 | | tp = = TP_ATR_x8 | | tp = = TP_ATR_x10 ) ;
if ( slOk & & tpOk )
return true ;
string bad = ( ! slOk ? " Stop-loss mode ( " + IntegerToString ( sl ) + " ) " : " " ) +
( ! slOk & & ! tpOk ? " and " : " " ) +
( ! tpOk ? " Take-profit mode ( " + IntegerToString ( tp ) + " ) " : " " ) ;
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. " ) ;
Print ( " Warrior EA: FIX - open the EA's Inputs tab, re-pick Stop-loss mode and Take-profit mode from "
" the dropdowns (the shipped pair is 'ATR * 1 from entry (classic)' and 'ATR * 3 from entry "
" (classic)'), then press OK. " ) ;
Print ( " Warrior EA: these two inputs define the neural network's TRAINING TARGET, not just order "
" placement - running with a wrong value would train the model on a strategy you did not choose, "
" so the EA stops here instead of guessing. " ) ;
Alert ( " Warrior EA: " + bad + " is invalid - re-pick Stop-loss / Take-profit mode in the Inputs tab. See the Experts log. " ) ;
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 |
//| account. Same reasoning as ValidateBarrierInputs() above: refuse to |
//| 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-01 11:27:28 -04:00
//| Apply the configuration shared by every AI architecture. |
//| |
//| MLP/CONV/LSTM/HYBRID take an IDENTICAL set of inputs - they differ|
//| only in the topology each builds inside its own InitIndicators(). |
//| This was four hand-copied blocks in OnInit() that had already |
//| drifted apart in indentation, which is exactly the shape where one|
//| architecture silently misses a setter the other three get and |
//| then trains on a different feature set or target than the Inputs |
//| tab claims - invisible until you compare two models' era metrics |
//| and cannot explain the gap. One body, four call sites. |
//| |
//| Takes the BASE pointer deliberately rather than a template: the |
//| four signal classes add nothing but a constructor and an |
//| InitIndicators() override, so every setter below already resolves |
//| on CExpertSignalAIBase (SLMode/TPMode come from its own base, |
//| CExpertSignalCustom). A template would only re-instantiate this |
//| identical body four times. |
//+------------------------------------------------------------------+
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.
2026-08-01 11:27:28 -04:00
aiSignal . MinDirectionalRecall ( MinRecall ) ;
aiSignal . LogitAdjustTau ( LogitAdjustTau / 100.0 ) ;
aiSignal . SignalClusterWindow ( SignalClusterWindow ) ;
aiSignal . FreezePriorCalibration ( FreezePriorCalibration ) ;
aiSignal . SwingConfirmationBars ( SwingConfirmationBars ) ;
//--- SL/TP reach the AI signals because they now define the TRAINING TARGET, not just the order.
//--- The triple-barrier label asks "does a trade with THIS stop and THIS target win from here",
//--- so these must be set before Expert.InitIndicators() builds the fingerprint and prebuilds the
//--- label cache - see CExpertSignalAIBase::TripleBarrierLabel/BarrierMultiples. InitializeSignal()
//--- sets them on the master signal only; the AI filters are separate objects.
aiSignal . SLMode ( ( int ) SL_Mode ) ;
aiSignal . TPMode ( ( int ) TP_Mode ) ;
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 . UseRSI ( EnableRSIFeature ) ;
aiSignal . UseMACD ( EnableMACDFeature ) ;
aiSignal . UseIchimoku ( EnableIchimokuFeature ) ;
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-01 11:27:28 -04:00
aiSignal . UseADCumulativeDelta ( EnableADCumulativeDelta ) ;
aiSignal . UseADShorteningOfThrust ( EnableADShorteningOfThrust ) ;
aiSignal . UseADWyckoffEventStream ( EnableADWyckoffEventStream ) ;
aiSignal . UseADWyckoffFailedStructure ( EnableADWyckoffFailedStructure ) ;
aiSignal . UseADWyckoffSignificantBarInversion ( EnableADWyckoffSignificantBarInversion ) ;
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 ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
2026-07-16 01:12:37 -04:00
//+------------------------------------------------------------------+
//| IMPORTANT: no failure branch below (nor in any helper it calls - |
//| AddFilterToSignal(), InitializeSignal(), InitializeTrailing(), |
//| InitializeMoneyManagement()) may call Expert.Deinit() before |
//| returning INIT_FAILED/false. MQL5 ALWAYS calls this EA's own |
//| OnDeinit(REASON_INITFAILED) automatically once OnInit() returns |
//| anything other than INIT_SUCCEEDED, and OnDeinit() already calls |
//| Expert.Deinit() itself. Expert.Deinit() tears down `signal` and, |
2026-07-27 22:08:55 -04:00
//| through it, every registered AI signal (MLP/CONV/LSTM/HYBRID - see|
2026-07-16 01:12:37 -04:00
//| g_aiSignals' declaration comment) - calling it a second time here |
//| would free those objects while g_aiSignals[] still points at them, |
//| and OnDeinit()'s own PersistOnShutdown() loop over g_aiSignals[] |
//| would then dereference already-freed pointers. This is exactly |
//| what "invalid pointer access" during OnDeinit() after a failed |
//| OnInit() means if it ever recurs - the fix is to remove whichever |
//| inline Expert.Deinit() call was re-added, not to guard the loop. |
//+------------------------------------------------------------------+
2026-07-14 22:36:27 -04:00
int OnInit ( )
{
2026-07-17 21:28:59 -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
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
//--- 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.
int initLeftover = 0 ;
int initPurged = WarriorPurgeChartObjects ( 0 , false , initLeftover ) ;
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 "
" than assumed from the last shutdown. " , __FUNCTION__ , initPurged ,
( initLeftover > 0
? StringFormat ( " (%d of them needed a by-name delete after the bulk call) " , initLeftover )
: " " ) ) ;
2026-07-27 11:13:19 -04:00
SetStatusLabel ( " Warrior EA: initializing... " ) ;
2026-07-27 15:52:39 -04:00
Print ( __FUNCTION__ + " : build tag " + WARRIOR_BUILD_TAG ) ;
2026-08-09 14:51:59 -04:00
PrintFormat ( " %s: trade settings snapshot - AIType=%d Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d TrailingStrategy=%d MM_STRATEGY=%d " ,
2026-07-27 15:52:39 -04:00
__FUNCTION__ , ( int ) AIType , ( int ) Entry_Multiplier , ( int ) SL_Mode , ( int ) TP_Mode ,
2026-08-09 14:51:59 -04:00
( int ) TrailingStrategy , ( int ) MM_STRATEGY ) ;
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
//--- HARD GATE on the two inputs that define the training target. MetaTrader does NOT validate a saved
//--- enum input against its current members: a chart whose settings were saved by an older build keeps
//--- the old integer, and the EA receives a value that is not in the enum at all.
//--- This is not hypothetical. On 2026-07-31 TP_PREV_SWING (-101) was deleted from TAKE_PROFIT_MODE;
//--- charts saved before that kept -101, and on 2026-08-01 all four topologies trained ~250 eras against
//--- a 1:1 barrier instead of the intended 1:3, because -101 fell through BarrierMultiples()'s
//--- "keep the barrier well-formed" fallback and silently became slMult. Hours of training measuring a
//--- strategy nobody chose, with nothing in the log saying so.
//--- Since the triple-barrier relabel these two inputs ARE the label definition, so a wrong value here
//--- is not a bad trade setting - it is a wrong dataset. Refuse to start rather than substitute
//--- something plausible: a dead chart with an explicit message costs minutes, a silently mistrained
//--- model costs a night and can be mistaken for a result.
if ( ! ValidateBarrierInputs ( ) )
return INIT_FAILED ;
2026-08-02 12:25:20 -04:00
if ( ! ValidateRiskInputs ( ) )
return INIT_FAILED ;
//--- 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.
g_riskBudget . Configure ( EnableRiskGuard , MaxDailyLossPct , MaxDrawdownPct , MaxDrawdownIsTrailing ,
RiskDayResetHour , RiskPerTradeOfBudget , RiskGuardFlatten ,
( long ) Expert_MagicNumber , 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 ) ;
2026-07-27 11:13:19 -04:00
LoadSignalsVisibilityState ( ) ;
2026-07-14 22:36:27 -04:00
int maxRetryOnError = 5 ;
string functionName = __FUNCTION__ ;
// Initialize random seed based on the number of milliseconds since the system started
MathSrand ( GetTickCount ( ) ) ;
// Initialize expert
bool expertInitialized = false ;
for ( int tries = 0 ; ! expertInitialized & & tries < 5 ; + + tries )
{
if ( ! Expert .Init ( Symbol ( ) , Period ( ) , Expert_EveryTick , Expert_MagicNumber ) )
{
Print ( functionName + " : Failed initializing expert, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
expertInitialized = true ;
break ;
}
}
if ( ! expertInitialized )
{
Print ( functionName + " : Failed to initialize expert after retries " ) ;
return INIT_FAILED ;
}
Expert . OnChartEventProcess ( true ) ;
// Creating signal
PrintVerbose ( " Initializing Signal... " ) ;
CExpertSignalCustom * signal = NULL ;
for ( int tries = 0 ; signal = = NULL & & tries < 5 ; + + tries )
{
signal = new CExpertSignalCustom ;
if ( signal = = NULL )
{
Print ( functionName + " : Failed to initialize Signal, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
break ;
}
}
if ( signal = = NULL )
{
Print ( functionName + " : Failed to initialize Signal after retries " ) ;
return INIT_FAILED ;
}
InitializeSignal ( signal ) ;
// Initializing Database
if ( UseDatabaseRanking )
{
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
//--- CExpertSignalCustom::Direction()). A version mismatch wipes the Signals folder (dbm.Init),
//--- which is the intended migration: INSERTs carry the new column, so an old-schema file would
//--- fail every insert forever, and FetchTradeRecords binds columns by position.
const string dbVersion = " 3.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 ) )
{
Print ( functionName + " : Failed to initialize Database, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
dbInitialized = true ;
break ;
}
}
if ( ! dbInitialized )
{
Print ( functionName + " : Failed to initialize Database after retries " ) ;
return INIT_FAILED ;
}
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.
if ( ! journal .Init ( GetPointer ( dbm ) , Expert_MagicNumber ) )
{
Print ( functionName + " : Failed to initialize trade journal table " ) ;
return INIT_FAILED ;
}
2026-08-12 15:20:33 -04:00
//--- Meta_Labeling_Design.md S1: the pattern DB doubles as the meta-label training corpus.
2026-08-12 23:56:26 -04:00
//--- The stale-DB check is ALWAYS on in the tester (a forgotten wipe silently voids a whole
//--- corpus run); the full report - volume per family, closed fraction, and the measured
//--- GMT->server bar-time offset S2's label plumbing pins to - stays under VerboseMode.
MetaCorpusStaleCheck ( ) ;
2026-08-12 15:20:33 -04:00
if ( VerboseMode )
MetaCorpusReport ( ) ;
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
//--- 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.
journal . InitTrackingOnly ( Expert_MagicNumber ) ;
2026-07-16 00:56:33 -04:00
//+------------------------------------------------------------------+
2026-07-27 22:08:55 -04:00
//| AIType selects which single AI architecture this run trades/ |
//| trains: MLP, CONV, LSTM, or HYBRID. HYBRID is the fused stacked |
//| model, not a 2-of-3 ensemble. |
2026-07-16 00:56:33 -04:00
//| - PAI (CSignalPAI): plain multi-layer Perceptron - input layer |
//| feeds straight into the tapering Dense hidden-layer stack, no |
//| Conv/Pool/LSTM stage. The baseline/cheapest model. |
//| - CONV (CSignalCONV): Conv+Pool front-end ahead of the same |
//| tapering Dense stack - looks for local price-action patterns |
//| (candlestick/short-range shapes) before the dense layers see |
//| them. |
//| - LSTM (CSignalLSTM): a single LSTM layer ahead of the same |
//| tapering Dense stack - genuine forget/input/output-gated |
//| recurrence (see AI\Network.mqh's CNeuronLSTM/CNeuronLSTMOCL), |
//| for sequential/regime-dependent structure the other two can't |
//| see across bars. |
2026-07-27 22:08:55 -04:00
//| HYBRID uses the same AI voting path as the individual models, but |
//| inside one stacked topology: Conv+Pool front-end, then LSTM, then |
//| the common dense taper. Concurrency model: MQL5 is |
2026-07-16 00:56:33 -04:00
//| single-threaded per chart - OnTick()/OnTimer() never run |
2026-07-27 22:08:55 -04:00
//| re-entrantly, so AI signal evaluation never races inside this |
2026-07-16 00:56:33 -04:00
//| EA's own code; PollTraining() below just calls each in turn every |
//| timer tick. The one real concurrency-relevant boundary is the |
//| native compute backend (WarriorCPU.dll/WarriorDML.dll, see |
2026-07-27 22:08:55 -04:00
//| AI\Network.mqh) - each CNet (there can be up to 2 alive at once |
//| per AI signal: live+shadow net) gets its OWN |
2026-07-16 00:56:33 -04:00
//| opaque per-instance context handle with no shared/global DLL |
//| state, so a fault or watchdog-kill against one can never poison |
2026-07-16 13:51:28 -04:00
//| another's calls. Each CNet's WarriorCPU.dll worker pool is sized |
2026-07-29 15:33:37 -04:00
//| to a fixed small thread count (CPU_THREADS_PER_NETWORK, |
//| AI\Network.mqh) rather than to a share of the machine, even though |
//| several pools can be alive at once - see that constant's comment. |
//| Within a chart that is free: MQL5's single execution thread per |
//| chart means only one pool is EVER actively computing at a time. |
//| Across charts it is what keeps N charts from each claiming the |
//| whole box, without depending on how many are attached yet. |
2026-07-27 22:08:55 -04:00
//| Memory ownership: AI signal(s) are allocated here with `new` and |
2026-07-16 00:56:33 -04:00
//| registered into g_aiSignals[] for the control panel's benefit, but |
//| g_aiSignals[] does NOT own them - AddFilterToSignal() below adds |
//| each to `signal`'s own filter array (CExpertSignal::AddFilter()), |
//| which frees its elements on destruction; `signal` itself is owned |
//| by Expert (InitializeSignal() -> Expert.InitSignal()). So the |
//| actual free happens via Expert.Deinit() (see OnDeinit() below) |
2026-07-27 22:08:55 -04:00
//| tearing down signal -> its filter array -> the AI signal object, |
//| exactly once - g_aiSignals[] is never delete'd directly (grep |
2026-07-16 00:56:33 -04:00
//| this file has no `delete g_aiSignals` anywhere), so there is no |
//| double-free risk from the two arrays holding the same pointers. |
//+------------------------------------------------------------------+
feat(nn): derive dense depth, train on all history, pin the shape in .cfg
Completes the derived-topology work. Three inputs removed.
AIType loses its depth suffix - AI_MLP/AI_CONV/AI_LSTM/AI_HYBRID, five
entries instead of eight. Depth is now derived from the two endpoints
the taper already has to connect (derived first-layer width, output-tied
final width) at a 2x per-layer compression target, clamped [2..5].
Asking a user to pick a layer count while the code derives the widths
those layers taper between was asking for half a decision: at 64 units
tapering to 12, four layers compress by 1.4x per step and five by 1.3x,
so the extra depth bought no abstraction. On the shipping H1/10y default
the derivation lands on 3 layers - the depth that actually won Run 2.
StudyPeriods removed. There is no case for training on less data than
the broker provides at a ~6% directional base rate; the honest
generalization read comes from the OOS holdout, not from withholding
history. Training now starts at the earliest available bar, floored by
MinTrainYear, which answers a different question (excluding dubious
pre-history) and stays.
That required closing the hazard the old code documented: the capacity
budget now MEASURES the symbol's real bar count, and a topology derived
from a measurement would widen as history downloads. Both ends are now
pinned. Every derived value left the weights-filename fingerprint -
keying a filename on a measured quantity means the EA looks for a file
that does not exist, starts from era 0 and orphans a trained model,
silently, because a missing cache is the normal first-run state. The
shape lives in the .cfg instead, where LoadAndCompare now ADOPTS the
four derived fields rather than diffing them; a mismatch there would
discard a fully-trained model over nothing the user did. Two fields
appended to the .cfg for the conv/LSTM stages, length-guarded on read
because FileReadInteger past EOF returns 0 with no error.
ForceHiddenLayers, a compile-time constant like DebuggingMode, pins
depth for diagnostic comparisons. It joins the fingerprint only when
non-zero, so forced depths get their own files - sequential comparisons
only, not simultaneous from one .ex5.
Derived shape, H1/10y defaults (21 features x 20 bars): first layer 64,
3 dense, 8 conv filters, 16 LSTM units. The LSTM block halves from
~58k to ~28k weights.
Both builds compile 0 errors, 0 warnings. Re-keys existing models.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:05:40 -04:00
EnablePAI = ( AIType = = AI_MLP ) ;
EnableCONV = ( AIType = = AI_CONV ) ;
EnableLSTM = ( AIType = = AI_LSTM ) ;
EnableHYBRID = ( AIType = = AI_HYBRID ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
EnableMETA = ( AIType = = AI_META ) ;
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 ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
CSignalMETA * META = CreateSignalWithRetry < CSignalMETA > ( maxRetryOnError , EnableMETA ) ;
2026-07-14 22:36:27 -04:00
//--- register whichever AI signal instances this run created, so the control panel can drive
//--- training/weight actions on exactly this run's current config (never another AIType's files)
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 ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if ( EnableMETA & & META ! = NULL )
RegisterAISignal ( META ) ;
2026-07-22 17:17:23 -04:00
CSignalMA * MA = CreateSignalWithRetry < CSignalMA > ( maxRetryOnError , EnableMA ) ;
CSignalRSI * RSI = CreateSignalWithRetry < CSignalRSI > ( maxRetryOnError , EnableRSI ) ;
2026-07-26 18:33:12 -04:00
CSignalMACD * MACD = CreateSignalWithRetry < CSignalMACD > ( maxRetryOnError , EnableMACD ) ;
CSignalIchimoku * Ichimoku = CreateSignalWithRetry < CSignalIchimoku > ( maxRetryOnError , EnableIchimoku ) ;
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 ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if ( ( EnableMA & & MA = = NULL ) | | ( EnableRSI & & RSI = = NULL ) | | ( EnableMACD & & MACD = = NULL ) | | ( EnableIchimoku & & Ichimoku = = NULL ) | | ( EnablePAI & & PAI = = NULL ) | | ( EnableCONV & & CONV = = NULL ) | | ( EnableLSTM & & LSTM = = NULL ) | | ( EnableHYBRID & & HYBRID = = NULL ) | | ( EnableMETA & & META = = 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 " ) ;
return INIT_FAILED ;
}
// Set filter parameters
2026-08-02 12:25:20 -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). The filter is now a pure read of
//--- that object - see Signals\SignalRiskGuard.mqh for why an account-level hard limit cannot live in a
//--- once-per-bar signal callback.
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 ) ;
}
2026-07-22 17:17:23 -04:00
if ( EnableMA )
{
MA . PeriodMA ( PeriodMA ) ;
feat: add unified MA type support to indicator tuner
Add `MA_TYPE_PRESETS` enum covering advanced (ALMA, DEMA, ZLEMA, T3, Kalman) and standard (SMA, EMA, SMMA, LWMA) moving averages. Integrate `maType` and `bestMaType` into `CADIndicatorTuner` struct, update flatten/unflatten routines, and bump `AD_TUNE_PARAM_COUNT` to 33. This allows the auto-tuner to search over MA type alongside period, improving feature discovery.
2026-07-23 15:02:09 -04:00
MA . Method ( MA_Type ) ;
2026-07-22 17:17:23 -04:00
if ( ! UseDatabaseRanking )
MA . Weight ( 1 ) ;
}
if ( EnableRSI )
{
RSI . PeriodRSI ( PeriodRSI ) ;
if ( ! UseDatabaseRanking )
RSI . Weight ( 1 ) ;
}
2026-07-26 18:33:12 -04:00
if ( EnableMACD )
{
MACD . PeriodFast ( MACD_PeriodFast ) ;
MACD . PeriodSlow ( MACD_PeriodSlow ) ;
MACD . PeriodSignal ( MACD_PeriodSignal ) ;
if ( ! UseDatabaseRanking )
MACD . Weight ( 1 ) ;
}
if ( EnableIchimoku )
{
Ichimoku . PeriodTenkan ( Ichimoku_PeriodTenkan ) ;
Ichimoku . PeriodKijun ( Ichimoku_PeriodKijun ) ;
Ichimoku . PeriodSenkou ( Ichimoku_PeriodSenkou ) ;
if ( ! UseDatabaseRanking )
Ichimoku . Weight ( 1 ) ;
}
2026-07-14 22:36:27 -04:00
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 ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if ( EnableMETA )
{
ConfigureAISignal ( META ) ;
//--- AFTER the shared config (which stamps the 1/3-output const input): the meta head is a
//--- 2-output binary softmax - "did this candidate's trade win" - and both the topology and the
//--- weights-filename fingerprint key off this value. See Meta_Labeling_Design.md S2.
META . OutputNeuronsCount ( 2 ) ;
feat(meta): self-contained corpus - the META chart sweeps the real classic ladders over its own history
The user should not need a tester corpus run per symbol. Every pattern
condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on
`int idx = StartIndex()` with zero hardcoded indices (verified), so a
name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes
the EXACT live ladder code answer "what would you have fired at bar i" -
the silent-divergence trap that justified the DB corpus does not exist on
this path, and neither do the GMT-offset ambiguity, the DB row caps, or
the wipe procedure.
- CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() +
SweepPrepare(bars) (deep-resizes the shared price series); the four
classic signal classes override SweepPrepare to deep-resize their own
indicator buffers.
- CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run
Direction() shifted, harvest the per-side pattern slots + netVote into
the same corpus arrays the DB loader fills; entry=bar open so
MetaPrepareEra's resolution matches at offset +0 with zero price error.
DB corpus remains the fallback when classic filters are disabled.
- Warrior_EA.mq5: META gets the enabled classic filters as candidate
sources (family ids match the descriptor one-hot).
- UseDatabaseRanking default false -> true (user request): a META chart
journals + ranks out of the box.
Workflow per symbol is now: attach ONE chart with AIType=META (optionally
Meta_ExportDataset=true for the offline pool) - candidates, labels,
training and export all happen in place, ~10 seconds of sweep instead of a
tester run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:19:43 -04:00
//--- candidate sources for the on-chart ladder sweep (BuildCorpusBySweep): the meta corpus is
//--- generated by evaluating these real filters over this chart's own history, so a META chart
//--- is fully self-contained - no tester corpus run. Family ids match MetaFamilyName/the
//--- descriptor one-hot: 0=MA 1=RSI 2=MACD 3=Ichimoku.
if ( EnableMA )
META . AddCandidateSource ( MA , 0 ) ;
if ( EnableRSI )
META . AddCandidateSource ( RSI , 1 ) ;
if ( EnableMACD )
META . AddCandidateSource ( MACD , 2 ) ;
if ( EnableIchimoku )
META . AddCandidateSource ( Ichimoku , 3 ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
}
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 ;
2026-07-22 17:17:23 -04:00
filtersAdded & = ( EnableMA ? AddFilterToSignal ( signal , MA ) : true ) ;
filtersAdded & = ( EnableRSI ? AddFilterToSignal ( signal , RSI ) : true ) ;
2026-07-26 18:33:12 -04:00
filtersAdded & = ( EnableMACD ? AddFilterToSignal ( signal , MACD ) : true ) ;
filtersAdded & = ( EnableIchimoku ? AddFilterToSignal ( signal , Ichimoku ) : true ) ;
2026-07-14 22:36:27 -04:00
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 ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
//--- META registers like any AI filter so it gets InitIndicators + training scheduling; its votes
//--- are structurally 0 in S2 (dPrevSignal never leaves the sentinel - see SignalMETA.mqh header).
filtersAdded & = ( EnableMETA ? AddFilterToSignal ( signal , META ) : true ) ;
2026-07-14 22:36:27 -04:00
if ( ! filtersAdded )
{
Print ( functionName + " : Error loading filters " ) ;
return INIT_FAILED ;
}
bool filterSuccess = false ;
for ( int tries = 0 ; tries < maxRetryOnError ; + + tries )
{
if ( UseDatabaseRanking & & ! dbm . OpenDatabase ( ) )
{
Print ( functionName + " : Error opening database, retrying... " ) ;
RandomSleep ( ) ;
continue ;
}
if ( UseDatabaseRanking & & ! dbm . BeginTransaction ( ) )
{
Print ( functionName + " : Error starting transaction, retrying... " ) ;
dbm . CloseDatabase ( ) ; // Ensure the database is closed before retry
RandomSleep ( ) ;
continue ;
}
if ( UseDatabaseRanking & & ( ! dbm . CommitTransaction ( ) | | ! dbm . CloseDatabase ( ) ) )
{
Print ( functionName + " : Error committing transaction or closing database, retrying... " ) ;
RandomSleep ( ) ;
continue ;
}
filterSuccess = true ;
break ; // Success if all operations complete without error
}
if ( ! filterSuccess )
{
Print ( functionName + " : Failed after all retries " ) ;
return INIT_FAILED ; // Return failure if retries are exhausted
}
// Trailing logic
PrintVerbose ( " Initializing Trailing... " ) ;
bool trailingInitialized = false ;
for ( int tries = 0 ; ! trailingInitialized & & tries < maxRetryOnError ; + + tries )
{
if ( ! InitializeTrailing ( ) )
{
Print ( functionName + " : Failed to initialize Trailing, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
trailingInitialized = true ;
break ;
}
}
if ( ! trailingInitialized )
{
Print ( functionName + " : Failed to initialize Trailing after retries " ) ;
return INIT_FAILED ;
}
// Creation of money object
bool moneyManagementInitialized = false ;
for ( int tries = 0 ; ! moneyManagementInitialized & & tries < maxRetryOnError ; + + tries )
{
if ( ! InitializeMoneyManagement ( ) )
{
Print ( functionName + " : Failed to initialize Money Management, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
moneyManagementInitialized = true ;
break ;
}
}
if ( ! moneyManagementInitialized )
{
Print ( functionName + " : Failed to initialize Money Management after retries " ) ;
return INIT_FAILED ;
}
// Check all trading objects parameters
PrintVerbose ( " Validating settings... " ) ;
bool settingsValidated = false ;
for ( int tries = 0 ; ! settingsValidated & & tries < maxRetryOnError ; + + tries )
{
if ( ! Expert . ValidationSettings ( ) )
{
Print ( functionName + " : Failed to validate settings, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
settingsValidated = true ;
break ;
}
}
if ( ! settingsValidated )
{
Print ( functionName + " : Failed to validate settings after retries " ) ;
return INIT_FAILED ;
}
// Tuning of all necessary indicators
PrintVerbose ( " Initializing Indicators... " ) ;
bool indicatorsInitialized = false ;
for ( int tries = 0 ; ! indicatorsInitialized & & tries < maxRetryOnError ; + + tries )
{
if ( ! Expert . InitIndicators ( ) )
{
Print ( functionName + " : Failed to initialize Indicators, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
indicatorsInitialized = true ;
break ;
}
}
if ( ! indicatorsInitialized )
{
Print ( functionName + " : Failed to initialize Indicators after retries " ) ;
return INIT_FAILED ;
}
// 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. The (much less frequent) DB-ranking work is throttled separately inside
// OnTimer() below rather than by EventSetTimer's own interval, since MQL5 only supports one timer
// interval per program.
if ( UseDatabaseRanking )
Expert . OnTimerProcess ( true ) ;
bool timerSet = false ;
2026-07-25 02:01:05 -04:00
// 500ms: Train() only does up to TRAIN_TIME_BUDGET_MS (80ms) of work per call, then yields back
// here. This interval drives training only on quiet symbols / no-tick stretches (real tick flow
// drives it independently), so it just needs to be short enough that a quiet era doesn't crawl -
// it was 5s originally (era took ~2min for <2s of compute), then 250ms. 250ms made the on-chart
// control panel laggy/hard to drag: every 250ms the timer ran ~80ms of compute AND a ChartRedraw,
// which competed with the user's drag events. 500ms halves that redraw/compute contention (smooth
// drag) while a quiet era still advances at ~80ms busy / 500ms = a healthy duty cycle. EventSet-
// MillisecondTimer is needed for sub-second resolution; EventSetTimer only accepts whole seconds.
// DB-ranking work below is paced by its own g_lastDbRankingRun/DB_RANKING_INTERVAL_SECONDS check,
// not by this interval, so it still only runs ~hourly regardless of this change.
int timerInterval_ms = 500 ;
2026-07-14 22:36:27 -04:00
for ( int tries = 0 ; ! timerSet & & tries < maxRetryOnError ; + + tries )
{
2026-07-17 09:11:42 -04:00
if ( ! EventSetMillisecondTimer ( timerInterval_ms ) )
2026-07-14 22:36:27 -04:00
{
Print ( functionName + " : Error creating timer, retrying... " ) ;
RandomSleep ( ) ;
}
else
{
timerSet = true ;
break ;
}
}
if ( ! timerSet )
{
Print ( functionName + " : Failed to set timer after retries " ) ;
return INIT_FAILED ;
}
// Initialization successful
PrintVerbose ( " Initialization successful " ) ;
if ( ! CreateControlPanel ( ) )
Print ( functionName + " : WARNING - control panel failed to initialize; trading/training continue normally, "
" but no GUI panel will be available for this run " ) ;
2026-07-17 21:28:59 -04:00
//--- 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
2026-07-14 22:36:27 -04:00
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 ) ;
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 ;
}
// Called before optimization/backtesting in the strategy tester
int OnTesterInit ( )
{
IsBacktesting = true ;
return ( INIT_SUCCEEDED ) ;
}
// Called after EA optimization in the strategy tester
void OnTesterDeinit ( )
{
2026-07-26 12:12:14 -04:00
// OnTesterInit/OnTesterDeinit fire once per tester session, even for a single (non-optimization)
// backtest - and by the time this runs, that pass's own OnDeinit() has ALREADY executed (the
// terminal calls it automatically at the end of every pass), including its own dbm.Deinit().
// Calling dbm.Deinit()/OnDeinit(0) again here duplicated the whole shutdown path (panel/arrow
// cleanup, weight save, MarketBookRelease) a second time with a hardcoded/wrong reason code -
// just reset the tester-only flag here.
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 11:13:19 -04:00
Print ( __FUNCTION__ + " : shutting down - reason: " + reasonStr ) ;
2026-07-27 15:52:39 -04:00
bool isTesterRun = ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) ) ;
if ( ! isTesterRun )
2026-07-27 11:13:19 -04:00
SaveSignalsVisibilityState ( ) ;
2026-07-25 01:07:21 -04:00
//--- Clear the VISIBLE EA objects FIRST and fast - status label, then control panel, then arrows -
//--- BEFORE the slow/fragile weight save below. On the CPU-DLL box the recursive Net.Save (two full nets
//--- per signal) and the net teardown in Expert.Deinit() can run long enough for MT5 to force-terminate
//--- OnDeinit ("Abnormal termination"); whatever was ordered AFTER that point never ran, which is what
//--- left the status label stuck on the chart. ClearStatusLabel() is a single fast op and goes ABSOLUTELY
//--- first (it was the reported straggler) so it happens even if a later step stalls; the panel and arrow
//--- purge follow while still cheap. Only then the heavy persistence runs, best-effort, last.
ClearStatusLabel ( ) ;
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
//--- ORDER, 2026-08-09: the arrow purge now runs BEFORE ExtPanel.Destroy(), not after.
//--- ExtPanel.Destroy() is a CAppDialog teardown over an unbounded control tree - it is not the cheap
//--- bounded step this ordering rule is written for, and putting it ahead of the arrow cleanup was the
//--- same inversion the rule exists to prevent, one call earlier. Measured on the 2026-08-09 run: the
//--- CONV chart logged "OnDeinit: shutting down" at 13:29:58.202 and hit "Abnormal termination" at
//--- 13:30:03.002 - 4.8 s, against ~1.1 s for the three charts that completed - having reached NONE of
//--- its chart-signal cleanup, so its arrows stayed on screen. That 4.8 s is MetaTrader's OnDeinit
//--- budget expiring, not a fault: nothing threw, the process simply ran out of time in a step ahead
//--- of the cleanup. The panel needs no rescue if it is starved - MT5 removes an unloaded EA's own
//--- dialog objects regardless - whereas stranded arrows persist on the chart and are then adopted by
//--- the next model to attach (see SaveChartSignals, which rebuilds the sidecar by SCANNING).
//--- Each step below is timed so the log names the slow one instead of leaving it to be inferred from
//--- which message is missing.
ulong deinitT0 = GetMicrosecondCount ( ) ;
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 ( ) ;
2026-07-27 15:52:39 -04:00
for ( int i = 0 ; i < g_aiSignalCount ; i + + )
fix(chart): arrows survived the EA that drew them - persist, then clear
Reported: on deinit the panel and status label go, the signal arrows stay.
Two independent causes, both fixed here.
1. It was partly deliberate. ShutdownChartCleanup carried a second
behaviour selected by a `preserveChartArrows` flag derived from the
deinit reason: on RECOMPILE / PARAMETERS / CHARTCHANGE / TEMPLATE the
arrows were left on the chart on purpose, to avoid a reload flicker.
That branch IS the reported symptom, an operator cannot tell it apart
from a cleanup that failed, and it was outright wrong whenever the
reload changed the config - REASON_PARAMETERS means exactly that, and
the preserved arrows then belonged to a model the chart no longer
runs, with nothing marking them stale. It is gone, along with the flag
and m_purgeChartOnDestruct. One path now: persist, clear, restore on
the next attach.
2. Whatever remains was unfalsifiable. PurgeChart was a single
ObjectsDeleteAll(prefix) whose return value was discarded, with no
caller ever looking at the chart again - so "the arrows are still
there" and "the arrows were never there" produced identical evidence,
which is why the report survived three sessions. It now verifies:
after the bulk delete it walks the OBJ_ARROW-typed list (a handful of
objects, not the whole chart), deletes any surviving WarSig_ by name,
and says so. Costs one typed scan when the bulk delete works, which is
the normal case; names the root cause when it does not.
Every failure mode of SaveChartSignals was also silent - it returned void
and had three bare early returns. It returns bool now, logs the open
error with the filename, and the shutdown purge is CONDITIONAL on it: for
a converged model the chart objects are the only copy of its signal
history (nothing redraws them - the renderer runs per training era and a
deployed model has none left), so a chart left littered because the disk
write failed beats a clean chart bought by destroying the history. Either
way the log now says which happened.
Also states the user's rule once, where arrows come back rather than
across InitNeuralNetwork's several exits: no weights loaded for this
config => clear the sidecar and start visually clean. A fresh run must
not inherit calls it never made, and the first save would otherwise adopt
them (the sidecar is rebuilt by scanning the chart).
Compiles 0 errors / 0 warnings, standard and Market. Needs redeploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:28:34 -04:00
//--- Persist the drawn arrows to their sidecar and take them off the chart, on EVERY deinit reason.
//--- The reason code no longer selects a behaviour here: a recompile/parameter change used to leave
//--- them up, which is precisely the "EA removed its panel but its signals stayed" report, and was
//--- also wrong whenever the reload changed the config the arrows belonged to. They come back on the
//--- next attach, from disk, if a model for that config exists - see PersistAndClearChartSignals().
g_aiSignals [ i ] . ShutdownChartCleanup ( ) ;
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
ulong deinitTArrows = GetMicrosecondCount ( ) ;
//--- Destroy the control panel's own UI so CAppDialog removes its own objects cleanly (see
//--- Controls\Dialog.mqh). After the arrow purge now - see the ordering note above.
if ( ! isTesterRun )
ExtPanel . Destroy ( reason ) ;
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
//--- FINAL SWEEP, after every owner-driven teardown has had its turn. ExtPanel.Destroy walks an
//--- unbounded control tree and ClearStatusLabel clears text rather than guaranteeing object removal, so
//--- either can leave a straggler - and until now nothing looked afterwards. Cheap and bounded: three
//--- prefix deletes plus one object-list scan, which is the shape of work this ordering rule permits at
//--- this point. Arrows are excluded because ShutdownChartCleanup above already persisted and removed
//--- them, and re-deleting them here would race that sidecar write for no gain.
//--- This cannot make a starved deinit safe on its own - that is what the OnInit purge is for. It closes
//--- the ordinary case; OnInit closes the case where MetaTrader never let us finish.
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 ) ;
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
ulong deinitTPanel = GetMicrosecondCount ( ) ;
Print ( __FUNCTION__ + " : cleanup timings - arrows " + DoubleToString ( ( deinitTArrows - deinitT0 ) / 1000.0 , 0 ) +
" ms, panel " + DoubleToString ( ( deinitTPanel - deinitTArrows ) / 1000.0 , 0 ) +
" 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. " ) ;
//--- The MarkShutdown()/ShutdownChartCleanup() pair that used to sit here has moved ABOVE the panel
//--- teardown - see the ordering note there. It still runs BEFORE StopTraining(), which is the other
//--- half of the same rule: StopTraining() finalises an in-flight run, and FinalizeTrainRun() restores
//--- the best checkpoint and (used to) persist it - a full model write. The original order put that
//--- heavy save ahead of the cheap visible cleanup. Measured 2026-08-01: "Abnormal termination" 4.46 s
//--- after OnDeinit began, with the chart-signal cleanup logging 0.2 s AFTER MetaTrader had killed it.
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
//--- Only now stop training. FinalizeTrainRun() still runs (the best checkpoint is restored in memory and
//--- becomes what the save below writes), but its own persist is suppressed during shutdown - see the
//--- m_shutdownInProgress guard - so the deployed model is written exactly once, not twice.
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
//--- FLUSH, don't finalise. See FlushTrainRun(): an in-flight era was never scored, checkpointed or
//--- deployable, so finishing it and then writing two full nets per chart spends the whole deinit
//--- budget to preserve work that cannot be used - and overrunning the budget is what strands the
//--- arrows and panel in the chart profile, permanently. A model that has CONVERGED is different: its
//--- weights may carry live online-learning updates that exist nowhere else, so it keeps the old
//--- finalise-and-save path.
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-07-25 12:02:38 -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. Best-effort and LAST: if it stalls/faults the chart is already clean (cleanup ran above)
//--- AND the last completed era is already on disk (every era end and the periodic autosave both persist
//--- independently), and CNet::Save is atomic so a killed write can never damage the existing .nnw.
//--- NOTE: an earlier revision skipped this entirely for in-process reloads (REASON_RECOMPILE /
//--- CHARTCHANGE / PARAMETERS) on the theory that an over-budget save left the CPU-DLL holding the old
//--- net's tensors and made the reload fail to allocate layer 0. That theory was wrong - the real cause of
//--- "loaded 0 of N layers (failed at layer 0)" was CLayer::CreateElement no longer overriding
//--- CArrayObj::CreateElement, so the read path failed before touching any backend (see AI\Network.mqh).
//--- With that fixed there is no reason to throw away the in-progress era on every recompile.
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
//--- SKIPPED for a model still training - that is the point of the flush above. This save exists to
//--- carry a PARTIAL era across a restart, and it is the single slowest step in OnDeinit (two full nets
//--- per chart, recursively, on the CPU-DLL box). Paying it costs more than the era is worth: the era
//--- was never scored or checkpointed, and overrunning the budget kills the cleanup that has to run.
//--- A CONVERGED model still saves. Its weights can hold online-learning updates made since the last
//--- era boundary, and for a deployed model there is no era boundary coming to persist them.
2026-07-27 15:52:39 -04:00
if ( ! isTesterRun )
{
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 ( ) )
continue ;
2026-07-25 12:02:38 -04:00
if ( ! g_aiSignals [ i ] . PersistWeightsOnShutdown ( ) )
2026-07-27 15:52:39 -04:00
Print ( __FUNCTION__ + " : WARNING - failed to persist weights for signal index " + IntegerToString ( i ) + " on shutdown (reason: " + reasonStr + " ) " ) ;
}
}
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 ;
void OnTimer ( )
{
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 ( ) ;
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-07-25 16:39:11 -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. Watch for the transition and
//--- resync once, rather than repainting every 500ms tick for nothing.
bool deployedNow = AllTrainingDeployed ( ) ;
if ( deployedNow ! = g_lastDeployedState )
{
g_lastDeployedState = deployedNow ;
RefreshControlPanelLabels ( ) ;
}
2026-07-14 22:36:27 -04:00
if ( ! UseDatabaseRanking )
return ;
datetime now = TimeCurrent ( ) ;
if ( g_lastDbRankingRun ! = 0 & & now - g_lastDbRankingRun < DB_RANKING_INTERVAL_SECONDS )
return ;
g_lastDbRankingRun = now ;
Expert . OnTimer ( ) ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
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-07-26 10:59:46 -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. Worse, "new
//--- bar" in the tester means every SIMULATED bar, so this fired a full ~7MB model write per simulated
//--- hour (thousands of writes per backtest), spamming save failures and burning the entire run's time
//--- on I/O. A tester run is inference-only anyway (see m_inferenceOnly) - the weights never change, so
//--- 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 ( )
{
research: export the feature matrix and a raw OHLCV grid for offline work
The bottleneck on this project has never been the modelling - it is that
every hypothesis costs a compile, a deploy, an attach and a log read, and
answers exactly one question. Days have gone into questions that are
seconds of arithmetic once the data is in hand.
Adds a RESEARCH-ONLY build, gated behind WARRIOR_EXPORT_FEATURES and
never compiled into a shipped binary, which writes two things to
Common\Files\Warrior_EA\Research\ and then does nothing at all:
<symbol>_<tf>_features.csv - one row per bar: index, time, OHLC, ATR,
and the m_neuronsCount feature values. Exactly what the network sees.
The raw bars ride along on purpose: with OHLC and ATR offline, every
barrier geometry, horizon and in-trade target is recomputable without
MetaTrader in the loop.
<symbol>_<tf>_rates.csv - raw OHLCV across a grid of 8 symbols x 5
timeframes. The 26 engineered features only exist for the attached
chart (indicator handles bind to PERIOD_CURRENT); raw rates do not, so
ONE attach yields the whole research grid. The bar time also makes
session/hour/day-of-week derivable - the only inputs in play that are
not a transform of the same OHLCV series.
Safety, because this binary gets attached to a chart on a LIVE ACCOUNT to
reach real history:
- OnTick returns immediately, so Expert.OnTick() - the entire trading
path - is unreachable regardless of the AlgoTrading toggle, the
signal state or the inputs. Structurally incapable of sending an
order, not merely unlikely to.
- No config lock. It never trains and never saves a model, so it has
nothing to protect against a concurrent chart - and taking the lock
would make it refuse to start exactly when the config it wants to
read is already open, which is when it is most useful.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:49:57 -04:00
# ifdef WARRIOR_EXPORT_FEATURES
//--- RESEARCH BUILD: writes the feature matrix at init and does NOTHING else, ever. This binary gets
//--- attached to a chart on a LIVE ACCOUNT to reach real history, so it must be structurally incapable of
//--- sending an order - not merely unlikely to. Returning here means Expert.OnTick() (the entire trading
//--- path) is never reached, independently of the AlgoTrading toggle, the signal state or the inputs.
return ;
# endif
2026-07-14 22:36:27 -04:00
CheckAlgoTradingState ( ) ;
2026-08-02 12:25:20 -04:00
//--- FIRST, and before Expert.OnTick() can open anything. This is the whole point of moving the risk
//--- limits out of the signal pipeline: evaluated here they run at quote frequency, so a 4% daily limit
//--- is tested on every quote instead of once per bar (Expert_EveryTick ships as false, so the old
//--- in-signal check fired once an hour on H1 - see Variables\RiskBudget.mqh).
g_riskBudget . Update ( ) ;
2026-07-14 22:36:27 -04:00
AutosaveWeightsIfDue ( ) ;
Expert . OnTick ( ) ;
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 ( ) ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void OnChartEvent ( const int id ,
const long & lparam ,
const double & dparam ,
const string & sparam )
{
//--- 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 ( ) ) ;
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-14 22:36:27 -04:00
signal_obj . ConfidenceSource ( ( int ) Confidence_Source ) ;
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 ) ;
2026-07-27 22:08:55 -04:00
//--- Same two inputs feed BOTH engines, and now on ONE scale each. Open is purely the averaged-vote
//--- threshold - there is no separate AI entry floor any more (the AI expresses its confidence as its
//--- vote weight instead, see CExpertSignalAIBase::ConfidenceTier/m_pattern_0), so no
//--- MinSignalConfidence() call is set on the AI signals above. Close drives the rule-based exit and
2026-07-26 18:33:12 -04:00
//--- the AI early exit. See Min_Vote_Open's declaration comment (Variables\Inputs.mqh) for the shared
//--- 0-100 conviction scale, and for why open and close must stay independent of each other.
//--- No UseAIExit() call any more - the early-exit route has no separate on/off switch, because
//--- Min_Vote_Close = Disabled already lands here as 1.01 and there disables it by arithmetic.
2026-07-26 17:27:51 -04:00
signal_obj . AIExitThreshold ( Min_Vote_Close / 100.0 ) ;
signal_obj . ThresholdOpen ( ( int ) Min_Vote_Open ) ;
signal_obj . ThresholdClose ( ( int ) Min_Vote_Close ) ;
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 ( )
{
if ( TrailingStrategy = = TRAILING_STRATEGY_NONE )
{
// No trailing strategy selected
return true ;
}
else
if ( TrailingStrategy = = TRAILING_STRATEGY_ATR_x1 | |
TrailingStrategy = = TRAILING_STRATEGY_ATR_x2 | |
TrailingStrategy = = TRAILING_STRATEGY_ATR_x3 )
{
// ATR Trailing Strategy
double multiplier = 0 ;
if ( TrailingStrategy = = TRAILING_STRATEGY_ATR_x1 )
multiplier = 1 ;
else
if ( TrailingStrategy = = TRAILING_STRATEGY_ATR_x2 )
multiplier = 2 ;
else
if ( TrailingStrategy = = TRAILING_STRATEGY_ATR_x3 )
multiplier = 3 ;
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 ;
}
}
2026-07-22 13:33:56 -04:00
else
if ( TrailingStrategy = = TRAILING_STRATEGY_INTELLIGENT )
{
// Confidence-adaptive ATR trailing (see Trailing\TrailingIntelligent.mqh) - the ATR
// multiple is set per-check from live AI confidence, so no fixed Multiplier() here.
CTrailingIntelligent * trailing = new CTrailingIntelligent ;
if ( trailing = = NULL )
{
Print ( __FUNCTION__ + " : error creating intelligent trailing " ) ;
return false ;
}
if ( ! Expert . InitTrailing ( trailing ) )
{
Print ( __FUNCTION__ + " : error initializing intelligent trailing " ) ;
return false ;
}
}
2026-07-14 22:36:27 -04:00
// Add more trailing strategies if needed
return true ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool InitializeMoneyManagement ( )
{
string functionName = __FUNCTION__ ;
if ( MM_STRATEGY = = FIXED_RISK )
{
CMoneyFixedRisk * money = new CMoneyFixedRisk ;
if ( money = = NULL )
{
Print ( functionName + " : error creating money " ) ;
return false ;
}
if ( ! Expert . InitMoney ( money ) )
{
Print ( functionName + " : error initializing money " ) ;
return false ;
}
money . Percent ( Money_Risk_Percent ) ;
}
else
if ( MM_STRATEGY = = FIXED_LOT )
{
CMoneyFixedLot * money = new CMoneyFixedLot ;
if ( money = = NULL )
{
Print ( functionName + " : error creating money " ) ;
return false ;
}
if ( ! Expert . InitMoney ( money ) )
{
Print ( functionName + " : error initializing money " ) ;
return false ;
}
money . Lots ( Money_FixLot_Lots ) ;
}
else
if ( MM_STRATEGY = = INTELLIGENT )
{
CMoneyIntelligent * money = new CMoneyIntelligent ;
if ( money = = NULL )
{
Print ( functionName + " : error creating money " ) ;
return false ;
}
if ( ! Expert . InitMoney ( money ) )
{
Print ( functionName + " : error initializing money " ) ;
return false ;
}
money . Percent ( Money_Risk_Percent ) ;
2026-07-22 13:33:56 -04:00
//--- Intelligent MM is AI-driven by definition now (the old Use_AI_Lot_Sizing toggle was
//--- removed as redundant) - always scale risk% by confidence via the Kelly path.
money . UseAIConfidenceLotSizing ( true ) ;
2026-07-14 22:36:27 -04:00
money . ConfidenceSource ( ( int ) Confidence_Source ) ;
}
// Add more money management strategies if needed
return true ;
}
//+------------------------------------------------------------------+