Warrior_EA/Warrior_EA.mq5
AnimateDread 5e0317f09d feat(chart): signal marks become price LEVELS at the trigger, not arrows beside the candle
User request: 'move from arrows on lows and highs to small horizontal lines at the actual
prices the entry/exit would trigger, just a bit larger than the candles. dark green for
buy, dark red for sell.'

Every mark is now an OBJ_TREND segment with both anchors at one price and both rays off,
spanning 1.3 bar widths, drawn at the bar's CLOSE - the price a market order actually
fires at, and the exact entry TripleBarrierLabel assumes. It used to sit on the candle's
LOW for a Buy and its HIGH for a Sell: prices the trade never touches, picked so an arrow
glyph would clear the candle. The tooltip now carries that price too.

COLOUR NOW MEANS DIRECTION AND ONLY DIRECTION on every layer (dark green / dark red).
Layer moves to width+style - the traded vote is solid and thick and drawn in front, a
single model's raw opinion is thin, dotted and behind the candles - which keeps the
distinction the old palette existed to draw (a model's opinion must never read as a trade)
while freeing colour to say one thing consistently.

Consequences handled, all of them the same 'a typed scan went blind' failure:
- SaveChartSignals filtered OBJPROP_TYPE == OBJ_ARROW and read OBJPROP_ARROWCODE. It now
  filters OBJ_TREND and recovers direction from the colour. The sidecar keeps the old
  217/218 numbers as its buy/sell token deliberately, so existing .arrows files still load.
- AdvanceChartSignalRestore now rebuilds through the SAME creation point the live path
  uses, so a restored mark and a fresh one are identical objects.
- The rescan-scoped delete enumerated ObjectsTotal(OBJ_ARROW) - retyped, or it silently
  deletes nothing.
- ApplySignalsVisibility enumerated OBJ_ARROW with NO prefix filter. Under the new type
  that would have hidden and shown THE USER'S OWN trend lines on every Hide/Show click;
  it is now prefix-scoped. The old type was uncommon enough on a real chart to mask the
  missing check - trend lines are the most hand-drawn object there is.
- DrawObject's high/low parameters are gone (6 call sites pass m_Close instead), so no
  caller can hand it a price it no longer draws at.
- Fixed a pre-existing stale comment that still described the purge sweep as OBJ_ARROW-only
  three lines above the note explaining it had been widened to every type.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:11:49 -04:00

2258 lines
126 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
//--- 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
//--- 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
//--- 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"
//--- chart-level tuned indicator periods: read at OnInit before the DB fingerprint and the classic
//--- signal configuration, written by a gated auto-tune install - see the file's header contract
#include "Variables\TunedPeriods.mqh"
//--- database classes
#include "Database\DatabaseManager.mqh"
#include "Database\TradeJournalManager.mqh"
//--- available custom classes
#include "Expert\ExpertCustom.mqh"
#include "Expert\AIBase\MetaCorpus.mqh"
#include "System\PrintVerbose.mqh"
#include "System\StatusLabel.mqh"
#include "System\AltDataFetch.mqh"
//--- 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"
//--- One-time "which instrument is this?" dialog for symbols the alt-data catalog does not know
#include "Panel\AltDataMapDialog.mqh"
//+------------------------------------------------------------------+
//| The CustomIndicators\*.mq5 files (ADCumulativeDelta, |
//| ADShorteningOfThrust, ADWyckoffEventStream, |
//| 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 |
//| 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). |
//| - 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. |
//+------------------------------------------------------------------+
//
CExpertCustom Expert;
CDatabaseManager dbm();
CTradeJournalManager journal;
//+------------------------------------------------------------------+
//| Pointers to whichever AI signal instances this run actually |
//| created (any enabled Use_* subset, plus the meta head), so the |
//| panel can drive training/weight actions on exactly the signal(s) |
//| in play this run and never touch another config's files. |
//+------------------------------------------------------------------+
//--- Must be >= the number of AI signal instances one run can create at once. That is FIVE since
//--- 2026-08-19: the four direction NNs are individually toggleable (Use_MLP/Use_CONV/Use_LSTM/
//--- Use_CONVLSTM) and the meta head (Use_MetaLabeling) runs BESIDE them rather than instead of
//--- them, so all five can be alive on one chart at once. At 3 the CONVLSTM member was once
//--- silently dropped on the floor (609be10); the array holds borrowed pointers, so headroom is
//--- free - but a cap that silently discards a model is a trapdoor, hence the loud refusal below.
#define MAX_AI_SIGNALS 5
//--- Printed at OnInit so tester logs prove which binary is actually running.
#define WARRIOR_BUILD_TAG "meta-gate-v1"
CExpertSignalAIBase *g_aiSignals[MAX_AI_SIGNALS];
int g_aiSignalCount = 0;
void RegisterAISignal(CExpertSignalAIBase *sig)
{
if(sig == NULL)
return;
//--- LOUD on overflow. This used to `return` silently alongside the NULL check, and that silence is
//--- the whole reason a dropped ensemble member survived undetected: an unregistered model still
//--- trains and still votes (it lives in the signal's own filter array), it just vanishes from every
//--- loop over g_aiSignals[] - the control panel buttons, PollTraining's wall-clock scheduling, the
//--- weight autosave, AltDataReload, the All*/Any* aggregates, and OnDeinit's MarkShutdown /
//--- ShutdownChartCleanup / FlushTrainRun. So it looks alive on the chart while being unreachable and
//--- unsaveable. A cap that silently discards a model is not a guard, it is a trapdoor.
if(g_aiSignalCount >= MAX_AI_SIGNALS)
{
Print(__FUNCTION__ + ": CANNOT REGISTER a further AI signal - MAX_AI_SIGNALS is " +
IntegerToString(MAX_AI_SIGNALS) + " and this run already created that many. The extra "
"model would still train and still vote, but the control panel could not reach it and its "
"weights would never be autosaved or flushed on shutdown. Raise MAX_AI_SIGNALS to at least "
"the number of instances the enabled Use_* inputs create and recompile.");
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. |
//+------------------------------------------------------------------+
//--- default spawn position: top-right corner, clear of the status label text block (top-left) so the
//--- 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;
//--- Alt-data maintenance + the one-time symbol-mapping dialog. Declared HERE rather than beside
//--- OnTimer because OnDeinit (further up the file) tears the dialog down, and MQL5 resolves global
//--- variables in declaration order.
#define ALTDATA_CHECK_SECONDS 1800
datetime g_lastAltDataRun = 0;
CAltDataFetch g_altDataFetch;
CAltDataMapDialog g_altMapDialog;
bool g_altMapDialogOpen = false;
bool g_altMapAsked = false; // one prompt per attach, even if the user closes it unanswered
bool g_signalsVisible = true;
#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;
}
//--- 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;
//--- 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.
//--- 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;
//--- 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;
//--- 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. several NNs enabled with one 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;
}
//--- "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;
}
void ApplySignalsVisibility(void)
{
//--- PREFIX-SCOPED, and that became mandatory on 2026-08-19 when the signal marks changed from
//--- OBJ_ARROW to OBJ_TREND (short horizontal levels - see WarriorPlotSignalLevel). Without the
//--- prefix test this loop would hide and show the USER'S OWN trend lines every time they
//--- clicked Hide/Show signals - their drawings, silently disappearing, blamed on the EA and
//--- rightly so. The old type was rare enough on a user's chart to mask the missing check;
//--- trend lines are the single most commonly hand-drawn object there is.
for(int i = ObjectsTotal(0, 0, OBJ_TREND) - 1; i >= 0; i--)
{
string name = ObjectName(0, i, 0, OBJ_TREND);
if(StringFind(name, SIG_ARROW_PREFIX) != 0)
continue;
ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
}
ChartRedraw(0);
}
void ToggleSignalsVisibility(void)
{
g_signalsVisible = !g_signalsVisible;
//--- Hide->Show is also the operator's manual "these arrows look stale" refresh: rescan each deployed
//--- model against recent history BEFORE re-showing, so Show Signals reveals a fresh set instead of
//--- just re-exposing whatever old render the .arrows sidecar happened to hold (see
//--- CExpertSignalAIBase::StartChartSignalRescan/AdvanceChartSignalRescan). 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).
if(g_signalsVisible)
{
bool anyQueued = false;
for(int i = 0; i < g_aiSignalCount; i++)
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()
}
}
ApplySignalsVisibility();
SaveSignalsVisibilityState();
}
//--- 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;
ApplySignalsVisibility();
RefreshControlPanelLabels();
Alert("Warrior EA: signal arrows shown");
}
//--- 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);
//--- 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"));
ChartRedraw(0);
}
//--- 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);
}
//--- 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;
//--- 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.
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))
{
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();
ClampControlPanelToChart();
//--- 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();
RefreshControlPanelLabels();
return true;
}
//--- 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\nThis action cannot be undone.", "Warrior EA - Confirm",
MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2) == IDYES;
}
//--- 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();
//--- 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"));
}
break;
case CP_ACTION_TOGGLE_PAUSE:
{
//--- 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.\nUse \"Retrain Model\" first if you want to train it further.");
break;
}
bool pause = !AllTrainingPaused();
for(int i = 0; i < g_aiSignalCount; i++)
if(pause)
g_aiSignals[i].PauseTraining();
else
g_aiSignals[i].ResumeTraining();
RefreshControlPanelLabels();
Alert("Warrior EA: training " + (pause ? "paused" : "resumed"));
break;
}
case CP_ACTION_TOGGLE_STOP:
{
//--- 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.\nUse \"Retrain Model\" to put it back into training.");
break;
}
bool doStop = !AllTrainingStopped();
for(int i = 0; i < g_aiSignalCount; i++)
if(doStop)
g_aiSignals[i].StopTraining();
else
g_aiSignals[i].StartTraining();
RefreshControlPanelLabels();
Alert("Warrior EA: training " + (doStop ? "stopped" : "restarted"));
break;
}
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.\nUse \"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\nDeploy 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)).\nTraining stopped; it now runs live inference. Click \"Retrain Model\" to train it further.");
break;
}
case CP_ACTION_SAVE:
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].SaveWeightsNow();
Alert("Warrior EA: weights saved");
break;
case CP_ACTION_LOAD:
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].LoadWeightsNow();
//--- A reload carries the saved file's own "deployed" flag, so it can flip the whole training
//--- state machine (loading a finalised model makes Deploy read "Retrain Model", and Pause/Stop
//--- read "(deployed)"). Resync the labels or the panel would keep offering stale actions.
RefreshControlPanelLabels();
Alert("Warrior EA: weights reloaded from disk");
break;
case CP_ACTION_RESET:
{
//--- CENSUS BEFORE THE ACTION. Every panel button loops over g_aiSignals[], and none of them said
//--- how many members it reached - so a four-member ensemble reset and a one-member reset produced
//--- identical output, and "it only wiped the first model" could be neither confirmed nor refuted.
//--- Printed BEFORE the confirmation dialog blocks, so the registry is on record even if the user
//--- then cancels. If this undercounts the enabled NNs, the fault is in REGISTRATION (see
//--- RegisterAISignal's MAX_AI_SIGNALS message), not in the reset.
PrintFormat("%s: RESET requested - %d AI signal(s) registered on this chart:",
__FUNCTION__, g_aiSignalCount);
for(int i = 0; i < g_aiSignalCount; i++)
PrintFormat("%s: [%d] %s", __FUNCTION__, i, g_aiSignals[i].RegistryLine());
if(!ConfirmDestructiveAction("Delete the saved AI weights of all " +
IntegerToString(g_aiSignalCount) +
" AI model(s) on this chart and restart training from era 0?"))
{
Alert("Warrior EA: weights reset cancelled");
break;
}
//--- ResetWeights() returns whether the fresh topology was REBUILT, and that return has been
//--- discarded since it was written. A member whose rebuild fails has had its files deleted and
//--- has no network - the one state the operator most needs told about - and the Alert still
//--- said "weights reset" for the whole chart. Count it, and say so when they disagree.
int resetOk = 0;
for(int i = 0; i < g_aiSignalCount; i++)
if(g_aiSignals[i].ResetWeights())
resetOk++;
RefreshControlPanelLabels();
PrintFormat("%s: RESET complete - %d of %d AI signal(s) rebuilt a fresh topology.",
__FUNCTION__, resetOk, g_aiSignalCount);
if(g_aiSignalCount == 0)
Alert("Warrior EA: nothing to reset - no AI signal is registered on this chart.");
else
if(resetOk == g_aiSignalCount)
Alert("Warrior EA: weights reset for " + IntegerToString(resetOk) +
" model(s) - training restarts from era 0");
else
Alert("Warrior EA: weights reset INCOMPLETE - " + IntegerToString(resetOk) + " of " +
IntegerToString(g_aiSignalCount) + " model(s) rebuilt.\nThe rest had their files "
"deleted but could not rebuild a topology - see the Experts log.");
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)
{
if(!ConfirmDestructiveAction("Delete the trade-journal and pattern-confidence database?"))
{
Alert("Warrior EA: database reset cancelled");
break;
}
//--- ResetDatabase() returns false when the file could not be deleted or the connection could
//--- not be reopened, and that return was discarded too - the Alert said "database reset"
//--- either way. One DB serves every signal on the chart (it is keyed by symbol/period/filter,
//--- not per model), so unlike the weights reset there is nothing per-member to loop over -
//--- but the registry is named anyway, because "does this touch the ensemble" is exactly the
//--- question being asked and the answer is "there is only one database to touch".
bool dbReset = dbm.ResetDatabase();
PrintFormat("%s: DATABASE RESET %s - one shared trade-journal/pattern-confidence DB serves all"
" %d AI signal(s) on this chart; per-model weights are NOT affected (use Delete &&"
" Reset Weights for those).", __FUNCTION__,
(dbReset ? "succeeded" : "FAILED"), g_aiSignalCount);
Alert(dbReset
? "Warrior EA: database reset"
: "Warrior EA: database reset FAILED - see the Experts log");
}
else
{
Print(__FUNCTION__ + ": database reset requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
Alert("Warrior EA: database reset requires \"Weight filters by DB win-rate\" to be enabled");
}
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)
}
//+------------------------------------------------------------------+
//| The DB fingerprint's first slot - was (int)AIType until the |
//| preset selector was replaced by the per-NN toggles (2026-08-19). |
//| Every configuration the OLD enum could express maps to ITS OWN |
//| OLD VALUE, so no existing database is re-keyed by the input |
//| change alone; subsets the old selector could not express (2-3 |
//| direction NNs) are genuinely new configs and get 100+bitmask, |
//| which cannot collide with the legacy 0-6 range. The meta gate |
//| deliberately does not key the slot beyond the legacy solo-META |
//| case: it FILTERS trades like EnableNewsFilter (also unhashed) - |
//| it does not change what a Pattern_N row means. |
//+------------------------------------------------------------------+
int DbLegacyAiSlot()
{
int bits = (Use_MLP ? 1 : 0) | (Use_CONV ? 2 : 0) | (Use_LSTM ? 4 : 0) | (Use_CONVLSTM ? 8 : 0);
if(bits == 15)
return 6; // all four = the old AI_HYBRID ensemble preset
if(bits == 0)
return Use_MetaLabeling ? 5 : 0; // legacy AI_META / AI_NONE
if(bits == 1)
return 1; // AI_MLP
if(bits == 2)
return 2; // AI_CONV
if(bits == 4)
return 3; // AI_LSTM
if(bits == 8)
return 4; // AI_CONVLSTM
return 100 + bits; // new subset - outside the legacy value space by construction
}
//+------------------------------------------------------------------+
//| 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()
{
//--- 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.
//--- LstmHiddenSize and ConvFilterCount left for the same reason 2026-07-30 (see
//--- ComputeLstmHiddenSize/ComputeConvFilterCount) - both are now functions of the feature flags and
//--- 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.
//--- 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",
DbLegacyAiSlot(), (int)OutputNeuronsCount, (int)TrainingOptimizer,
//--- LEGACY SLOT (was ind_Periods, derived since 2026-08-11). The literal
//--- is the shipped default so every existing database keeps its key.
20,
EnableVolume, EnableTime, EnableATR,
//--- TUNED values, not the seed constants (2026-08-16): the classic votes
//--- run at the adopted periods, so the DB key must describe THOSE - a
//--- tuning install re-keys the database, which is the semantics rule
//--- (new periods = new pattern definitions = fresh win-rate history).
EnableMA, g_TunedMaPeriod, g_TunedMaType, EnableRSI, g_TunedRsiPeriod,
EnableSwingContext, EnableNews,
EnableADCumulativeDelta, EnableADShorteningOfThrust, EnableADWyckoffEventStream)
+ StringFormat("|%d|%d|%d|%d", EnableADWyckoffFailedStructure, EnableADWyckoffSignificantBarInversion,
EnableMAFeature, EnableRSIFeature);
//--- 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,
g_TunedMacdFast, g_TunedMacdSlow, g_TunedMacdSignal);
if(EnableIchimoku || EnableIchimokuFeature)
fp += StringFormat("|ICHI:%d:%d:%d:%d:%d", EnableIchimoku, EnableIchimokuFeature,
g_TunedIchiTenkan, g_TunedIchiKijun, g_TunedIchiSenkou);
//--- Alt-data block, conditional like MACD/ICHI: enabling it changes what the model trades on, so it
//--- keys the database; only the FLAG goes in, never the per-symbol feature list - that is a measured
//--- property served by the AltData file and pinned per-model in the .cfg, and a measured quantity
//--- must not key a filename (same rule as the cross-asset pair set).
if(EnableAltData)
fp += "|ALT:1";
//--- 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);
if(EnableSpreadFeature)
fp += StringFormat("|SPR:%d", EnableSpreadFeature);
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);
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| 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);
//--- HistoryBars is no longer seeded here: the window is DERIVED at InitNeuralNetwork (fresh
//--- model) or ADOPTED from the .cfg (existing model) - see DeriveHistoryBars.
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);
//--- The training target is no longer selectable - direction models are unconditionally triple-barrier
//--- (see the withdrawn TrainingTarget input in Variables\Inputs.mqh for why, and for the three lines
//--- that bring the fractal option back). CSignalMETA still claims its own target in its constructor;
//--- that path is untouched. Nothing to set here: the barrier target is the class default.
//--- Ensemble membership stamps the |ENS1 fingerprint token - set before InitIndicators for the same
//--- reason as the target above. TWO OR MORE enabled direction NNs = an ensemble (2026-08-19: the
//--- per-NN toggles replaced the AI_HYBRID preset; all-four reproduces the old token exactly, so
//--- existing ensemble weight files keep loading, and a lone member keeps the old solo fingerprint).
//--- The meta head is EXCLUDED by target: it casts no direction votes, so it is not a vote member,
//--- not part of the era barrier, and its fingerprint stays compatible with existing solo-META files.
if(!aiSignal.IsMetaTarget()
&& (EnablePAI ? 1 : 0) + (EnableCONV ? 1 : 0) + (EnableLSTM ? 1 : 0) + (EnableHYBRID ? 1 : 0) >= 2)
//--- second argument mirrors the open threshold into the combined-vote OOS scorer so the
//--- ensemble panel's "Ensemble vote" line fires on the same criterion the live trade does
aiSignal.EnsembleMember(true, (double)Min_Vote_Open);
//--- THE EXIT POLICY, pushed into the AI signal so the GATE reads the same two inputs the live order path
//--- does. The deploy gate certifies a win rate measured on hold-to-resolution trades; every vote-driven
//--- exit closes earlier than that, so without this the certificate and the trade can silently describe
//--- different games (the 2026-08-09 geometry incident, in a different disguise). Min_Vote_Close =
//--- Disabled arrives as 1.01 and switches the vote exit off by arithmetic on this side too, so the
//--- shipped default replays as SL/TP-only and the simulation is arithmetically the certified trade.
aiSignal.ExitPolicy((double)Min_Vote_Close, false);
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);
aiSignal.UseCrossAsset(EnableCrossAsset);
aiSignal.UseSpreadFeature(EnableSpreadFeature);
aiSignal.UseAltData(EnableAltData);
aiSignal.UseADCumulativeDelta(EnableADCumulativeDelta);
aiSignal.UseADShorteningOfThrust(EnableADShorteningOfThrust);
aiSignal.UseADWyckoffEventStream(EnableADWyckoffEventStream);
aiSignal.UseADWyckoffFailedStructure(EnableADWyckoffFailedStructure);
aiSignal.UseADWyckoffSignificantBarInversion(EnableADWyckoffSignificantBarInversion);
aiSignal.AutoTuneIndicators(AutoTuneIndicators);
}
//+------------------------------------------------------------------+
// 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;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 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, |
//| through it, every registered AI signal (MLP/CONV/LSTM/HYBRID - see|
//| 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. |
//+------------------------------------------------------------------+
int OnInit()
{
//--- FIRST: adopt the chart's tuned indicator periods (if a gated auto-tune install ever wrote them).
//--- Must precede ComputeDbConfigFingerprint() and the classic-signal configuration below, both of
//--- which consume the g_Tuned* values - see Variables\TunedPeriods.mqh for the whole contract.
LoadTunedPeriods();
//--- clears out whatever status label text was left over from before this OnInit() ran (stale text
//--- from a prior "warm" re-init - e.g. an input-parameter change, which reuses this same running
//--- instance rather than a fresh one - would otherwise sit unchanged and look like nothing is
//--- happening) so it's obvious the moment training/signal init actually resumes producing new status text
//--- START FROM A GUARANTEED-CLEAN CHART. Before anything is drawn, sweep every object namespace this
//--- EA owns (WarriorChartPrefixes). Chart objects live in the chart PROFILE, not in the EA, so they
//--- outlive the process: a deinit that ran out of MetaTrader's ~4,500 ms budget, a crash, a terminal
//--- kill, or an .ex5 replaced while attached all leave objects behind that no later deinit will ever
//--- own. Deleting the EA's files does not remove them either, which is why they read as corruption.
//--- Arrows are INCLUDED in this sweep: LoadChartSignals restores them from their sidecar moments later
//--- and already opens with its own arrow sweep, so purging here costs nothing and removes any orphan
//--- that the sidecar does not account for - the ones that would otherwise be adopted by the next model
//--- to attach, because SaveChartSignals rebuilds that sidecar by SCANNING the chart.
//--- Runs BEFORE SetStatusLabel below, or it would delete the label it just created.
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)
: ""));
//--- AND SAY WHAT SURVIVED IT. "Removed N, zero by-name leftovers" only ever meant "nothing matching OUR
//--- PREFIXES remains" - it was never a statement about the chart, and on 2026-08-17 22:00 all three
//--- charts printed exactly that and still came up with duplicated panels. The purge cannot delete what
//--- it cannot name, so the one thing worth logging is the residue it did not claim: if a stale panel is
//--- sitting there under a name this list has never heard of, its name is now in the journal instead of
//--- being inferred from a screenshot. Names, not just a count - a count cannot be acted on.
//--- Deliberately NOT deleted: an unmatched object may be the user's own drawing or another indicator's,
//--- and this EA does not remove what it did not create. Reporting is the whole intervention.
{
string resPrefixes[];
int np = WarriorChartPrefixes(resPrefixes);
int resTotal = ObjectsTotal(0, -1, -1);
string residue = "";
int unmatched = 0;
for(int i = 0; i < resTotal; i++)
{
string nm = ObjectName(0, i, -1, -1);
bool ours = false;
for(int q = 0; q < np; q++)
if(StringFind(nm, resPrefixes[q]) == 0)
{
ours = true;
break;
}
if(ours)
continue;
unmatched++;
if(unmatched <= 12)
residue += (residue == "" ? "" : ", ") + nm;
}
if(unmatched > 0)
PrintFormat("%s: chart residue after the init purge - %d object(s) this EA did not create and did"
" not touch: %s%s. If a Warrior panel or status line is VISIBLE on the chart and is"
" not in this list and was not removed above, the prefix list has drifted again"
" (see WarriorChartPrefixes).", __FUNCTION__, unmatched, residue,
(unmatched > 12 ? StringFormat(" ... and %d more", unmatched - 12) : ""));
}
SetStatusLabel("Warrior EA: initializing...");
//--- WHICH BINARY IS ACTUALLY RUNNING. The hand-edited tag alone could not answer that - it only moves
//--- when someone remembers to move it, and it had sat at "scan-nofwd-v5" across a week of commits, so a
//--- run that predated a fix and a run that included it printed the identical line. __DATETIME__ is
//--- stamped by MetaEditor at COMPILE time and cannot be forgotten: if this timestamp is older than the
//--- commit you are testing, the .ex5 on disk is stale and nothing below this line is evidence about the
//--- current source. Read it FIRST when a fix appears not to have taken.
PrintFormat("%s: build tag %s | COMPILED %s", __FUNCTION__, WARRIOR_BUILD_TAG,
TimeToString(__DATETIME__, TIME_DATE | TIME_MINUTES));
PrintFormat("%s: trade settings snapshot - NNs=%s Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d TrailingStrategy=%d MM_STRATEGY=%d",
__FUNCTION__, EnabledNNSummary(), (int)Entry_Multiplier, (int)SL_Mode, (int)TP_Mode,
(int)TrailingStrategy, (int)MM_STRATEGY);
//--- 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;
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());
g_riskBudget.ConfigureExpectancy(ExpectancyMinTrades, ExpectancySigma);
g_riskBudget.Update();
if(EnableRiskGuard)
Print("Warrior EA: ", g_riskBudget.StatusLine());
//--- 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);
LoadSignalsVisibilityState();
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);
//--- Alt-data MUST be on disk BEFORE any model is built below, not after: InitNeuralNetwork's
//--- fingerprint/input-width pinning happens once, at construction, and never re-widens for data
//--- that lands later (see AltDataReload's declaration comment for the OLD failure - a fresh
//--- attach with an empty AltData folder built every model at 0 features while the OnTimer-driven
//--- download finished moments afterward, and every model then trained on price alone for the rest
//--- of the run). Blocking here instead of leaving it to the timer means the one-off backfill
//--- WebRequest(s) (seconds, per CAltDataFetch::Update's comment) delay this OnInit instead of
//--- racing it - a slower attach is the correct trade against a silently undersized model.
//--- Skipped for an unmapped symbol (Update() no-ops via FindSpec) and in the tester/optimizer,
//--- where WebRequest is unavailable by platform rule - same guard OnTimer's upkeep tick uses.
if(EnableAltData && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
{
g_lastAltDataRun = TimeCurrent();
g_altDataFetch.Update(_Symbol);
}
//--- Same fix, same reason, for the cross-asset reference pairs (System\CrossAsset.mqh): a fresh
//--- model's first successful Build() pins whichever reference pairs happened to be synchronised at
//--- that instant, for the model's life (see WarmBlocking's declaration comment) - so a pair still
//--- mid-sync at attach time would be silently and permanently excluded, exactly the alt-data failure
//--- above. Skipped in the tester/optimizer, where the agent loads auxiliary symbols from the
//--- terminal rather than syncing them, so waiting here would only burn the timeout for nothing.
if(EnableCrossAsset && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
{
CCrossAssetPanel crossAssetWarmer;
//--- A TIMEOUT HERE MUST BE AUDIBLE. If the wait expires, the very pin this call exists to
//--- protect happens anyway, one pair short and permanently - i.e. the silent failure mode is
//--- back, and the only difference is that we waited 4s for it. Say so in the journal, because
//--- from that point on nothing else in the run ever mentions the missing pair again.
if(!crossAssetWarmer.WarmBlocking((ENUM_TIMEFRAMES)Period(), 4000))
Print("Cross-asset warm-up did not finish syncing every reference pair within 4s - the model"
" built below will PIN whichever pairs are ready at that moment and never add the rest."
" If the feature count looks short, detach and re-attach once the terminal has finished"
" downloading the reference symbols' history.");
}
// 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"};
//--- 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";
//--- 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.
//--- 4.0 (2026-08-19): DB timestamps switched from GMT to BROKER time (user decision: one
//--- clock everywhere). The bases were already MIXED - live journaling stamped GMT while the
//--- online-learning backfill stamped bar/server time - so the newest-row duplicate guard
//--- could silently reject a live row landing within ~3h after a backfill row. The bump
//--- wipes the Signals store: the only honest reset for a corpus with two clocks in one
//--- column.
const string dbVersion = "4.0";
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;
}
//--- 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;
}
//--- Meta_Labeling_Design.md S1: the pattern DB doubles as the meta-label training corpus.
//--- 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();
if(VerboseMode)
MetaCorpusReport();
}
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);
//+------------------------------------------------------------------+
//| The per-NN inputs (Use_MLP/Use_CONV/Use_LSTM/Use_CONVLSTM) pick |
//| which direction architectures this run trades/trains - any |
//| subset, each an independent model - plus the optional meta head |
//| (Use_MetaLabeling). The architectures: |
//| - 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. |
//| 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 |
//| single-threaded per chart - OnTick()/OnTimer() never run |
//| re-entrantly, so AI signal evaluation never races inside this |
//| 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 |
//| AI\Network.mqh) - each CNet (there can be up to 2 alive at once |
//| per AI signal: live+shadow net) gets its OWN |
//| opaque per-instance context handle with no shared/global DLL |
//| state, so a fault or watchdog-kill against one can never poison |
//| another's calls. Each CNet's WarriorCPU.dll worker pool is sized |
//| 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. |
//| Memory ownership: AI signal(s) are allocated here with `new` and |
//| 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) |
//| tearing down signal -> its filter array -> the AI signal object, |
//| exactly once - g_aiSignals[] is never delete'd directly (grep |
//| this file has no `delete g_aiSignals` anywhere), so there is no |
//| double-free risk from the two arrays holding the same pointers. |
//+------------------------------------------------------------------+
//--- Re-derived from the per-NN inputs (they are also initialized at file scope in
//--- Variables\Variables.mqh; this re-assignment is the init-order-safe truth). EnableHYBRID still
//--- names the CONVLSTM topology INSTANCE (CSignalHYBRID, State\HYBRID\) - a historical rename
//--- kept so the class and its on-disk files keep their identity.
EnablePAI = Use_MLP;
EnableCONV = Use_CONV;
EnableLSTM = Use_LSTM;
EnableHYBRID = Use_CONVLSTM;
EnableMETA = Use_MetaLabeling;
// Creating instances of signals
CSignalPAI *PAI = CreateSignalWithRetry<CSignalPAI>(maxRetryOnError, EnablePAI);
CSignalCONV *CONV = CreateSignalWithRetry<CSignalCONV>(maxRetryOnError, EnableCONV);
CSignalLSTM *LSTM = CreateSignalWithRetry<CSignalLSTM>(maxRetryOnError, EnableLSTM);
CSignalHYBRID *HYBRID = CreateSignalWithRetry<CSignalHYBRID>(maxRetryOnError, EnableHYBRID);
CSignalMETA *META = CreateSignalWithRetry<CSignalMETA>(maxRetryOnError, EnableMETA);
//--- 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 config's files)
g_aiSignalCount = 0;
//--- The meta gate pointer must not survive a re-init: an input change re-enters OnInit in the
//--- same program instance, and the old object is freed by the filter-array teardown.
g_warriorMetaGate = NULL;
if(EnablePAI && PAI != NULL)
RegisterAISignal(PAI);
if(EnableCONV && CONV != NULL)
RegisterAISignal(CONV);
if(EnableLSTM && LSTM != NULL)
RegisterAISignal(LSTM);
if(EnableHYBRID && HYBRID != NULL)
RegisterAISignal(HYBRID);
if(EnableMETA && META != NULL)
{
RegisterAISignal(META);
//--- Arm the vote-pipeline meta gate (CSignalMETA::LiveMetaGate): CheckOpenPosition consults
//--- this pointer on every vote-cleared entry, and the ensemble era verdict replays the same
//--- veto over its OOS fired bars so certified == traded.
g_warriorMetaGate = META;
}
CSignalMA *MA = CreateSignalWithRetry<CSignalMA>(maxRetryOnError, EnableMA);
CSignalRSI *RSI = CreateSignalWithRetry<CSignalRSI>(maxRetryOnError, EnableRSI);
CSignalMACD *MACD = CreateSignalWithRetry<CSignalMACD>(maxRetryOnError, EnableMACD);
CSignalIchimoku *Ichimoku = CreateSignalWithRetry<CSignalIchimoku>(maxRetryOnError, EnableIchimoku);
CSignalNewsFilter *newsFilter = CreateSignalWithRetry<CSignalNewsFilter>(maxRetryOnError, EnableNewsFilter);
CSignalSessionFilter *sessionFilter = CreateSignalWithRetry<CSignalSessionFilter>(maxRetryOnError, EnableSessionFilter);
//--- 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).
CSignalRiskGuard *riskGuard = CreateSignalWithRetry<CSignalRiskGuard>(maxRetryOnError, EnableRiskGuard);
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))
{
Print("Critical signal initialization failed, cannot proceed");
return INIT_FAILED;
}
// Set filter parameters
//--- 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.
if(EnableSessionFilter)
{
sessionFilter.TradeLondonSession(SF_trade_LondonSession);
sessionFilter.TradeNewYorkSession(SF_trade_NewYorkSession);
sessionFilter.TradeTokyoSession(SF_trade_TokyoSession);
}
//--- Classic votes run at the chart's TUNED periods (g_Tuned*, loaded first thing in OnInit) -
//--- the same values the AI features seed from and the DB key hashes, so all three consumers
//--- always describe the same indicators. See Variables\TunedPeriods.mqh.
if(EnableMA)
{
MA.PeriodMA(g_TunedMaPeriod);
MA.Method(g_TunedMaType);
if(!UseDatabaseRanking)
MA.Weight(1);
}
if(EnableRSI)
{
RSI.PeriodRSI(g_TunedRsiPeriod);
if(!UseDatabaseRanking)
RSI.Weight(1);
}
if(EnableMACD)
{
MACD.PeriodFast(g_TunedMacdFast);
MACD.PeriodSlow(g_TunedMacdSlow);
MACD.PeriodSignal(g_TunedMacdSignal);
if(!UseDatabaseRanking)
MACD.Weight(1);
}
if(EnableIchimoku)
{
Ichimoku.PeriodTenkan(g_TunedIchiTenkan);
Ichimoku.PeriodKijun(g_TunedIchiKijun);
Ichimoku.PeriodSenkou(g_TunedIchiSenkou);
if(!UseDatabaseRanking)
Ichimoku.Weight(1);
}
if(EnableNewsFilter)
{
newsFilter.SetMinImpact(NF_MinImpact);
newsFilter.SetLookbackMinutes(NF_LookMinutes);
}
if(EnablePAI)
ConfigureAISignal(PAI);
if(EnableCONV)
ConfigureAISignal(CONV);
if(EnableLSTM)
ConfigureAISignal(LSTM);
if(EnableHYBRID)
ConfigureAISignal(HYBRID);
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);
//--- 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);
}
// Add filters
PrintVerbose("Initializing Signal filters...");
//--- added exactly once, before the DB retry loop below - these calls don't depend on DB success at
//--- all (every pointer here was already validated non-NULL above), but living inside the loop body
//--- meant a DB open/transaction failure that triggered a retry would re-run AddFilterToSignal() and
//--- register the same filter pointer a second time in signal's CArrayObj; since that array frees its
//--- elements on destruction, a duplicate entry means the same pointer gets deleted twice on shutdown
//--- (heap corruption), which could easily explain instability across a later remove/re-add cycle.
bool filtersAdded = true;
filtersAdded &= (EnableMA ? AddFilterToSignal(signal, MA) : true);
filtersAdded &= (EnableRSI ? AddFilterToSignal(signal, RSI) : true);
filtersAdded &= (EnableMACD ? AddFilterToSignal(signal, MACD) : true);
filtersAdded &= (EnableIchimoku ? AddFilterToSignal(signal, Ichimoku) : true);
filtersAdded &= (EnableSessionFilter ? AddFilterToSignal(signal, sessionFilter) : true);
filtersAdded &= (EnableNewsFilter ? AddFilterToSignal(signal, newsFilter) : true);
filtersAdded &= (EnableRiskGuard ? AddFilterToSignal(signal, riskGuard) : true);
filtersAdded &= (EnablePAI ? AddFilterToSignal(signal, PAI) : true);
filtersAdded &= (EnableCONV ? AddFilterToSignal(signal, CONV) : true);
filtersAdded &= (EnableLSTM ? AddFilterToSignal(signal, LSTM) : true);
filtersAdded &= (EnableHYBRID ? AddFilterToSignal(signal, HYBRID) : true);
//--- 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);
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;
// 500ms: Train() only does up to TRAIN_TIME_BUDGET_MS (120ms solo, 30ms per ensemble member) 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;
for(int tries = 0; !timerSet && tries < maxRetryOnError; ++tries)
{
if(!EventSetMillisecondTimer(timerInterval_ms))
{
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");
//--- one-time cleanup: an earlier build used Comment() plus a separate background rectangle object
//--- that turned out to render ON TOP of the text (Comment() has no built-in background/styling
//--- parameters at all) - delete any leftover from a prior run now that status text is a single
//--- self-contained OBJ_LABEL (see SetStatusLabel()) with its own BGCOLOR fill instead
if(ObjectFind(0, "WarriorCommentBG") >= 0)
ObjectDelete(0, "WarriorCommentBG");
//--- required for CAppDialog's caption-bar drag to work at all - without it, the chart never delivers
//--- CHARTEVENT_MOUSE_MOVE and the panel silently ignores drag attempts
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
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()
{
// 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.
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
double totalTrades = TesterStatistics(STAT_TRADES);
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, |
//| 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)
{
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();
string reasonStr = DeinitReasonToString(reason);
Print(__FUNCTION__ + ": shutting down - reason: " + reasonStr);
bool isTesterRun = (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD));
if(!isTesterRun)
SaveSignalsVisibilityState();
//--- 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();
//--- EARLY VISIBLE-UI SWEEP, 2026-08-16. The 4,500ms deinit budget is measured from MetaTrader's stop
//--- REQUEST, not from OnDeinit's first line - so a heavy operation already in flight (the weight
//--- autosave, a warm-up MI scan) eats the window before teardown even begins. Measured today: autosave
//--- logged at 18:51:57.8, OnDeinit began 18:52:01.568, "Abnormal termination" at 18:52:02.000 - the
//--- whole teardown got ~430ms and died inside the first member's arrow persist, stranding the panel
//--- and every arrow. No ordering of the EXISTING steps survives that; what does is making the visible
//--- UI vanish in the first few milliseconds. skipArrows=true: this sweep takes the status label,
//--- control panel and mapping dialog (native prefix deletes, single-digit ms) - arrows are excluded
//--- because their sidecar write below has to SCAN them off the chart first.
int earlySweepLeft = 0;
WarriorPurgeChartObjects(0, true, earlySweepLeft);
//--- 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();
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].MarkShutdown();
for(int i = 0; i < g_aiSignalCount; i++)
//--- Persist the drawn arrows to their sidecar and take them off the chart, on EVERY deinit reason.
//--- 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();
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);
if(g_altMapDialogOpen)
{
g_altMapDialog.Destroy(reason);
g_altMapDialogOpen = false;
}
}
//--- 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);
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.
//--- 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.
//--- 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;
for(int i = 0; i < g_aiSignalCount; i++)
{
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.");
//--- 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.
//--- 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.
if(!isTesterRun)
{
for(int i = 0; i < g_aiSignalCount; i++)
{
if(!g_aiSignals[i].TrainingComplete())
continue;
if(!g_aiSignals[i].PersistWeightsOnShutdown())
Print(__FUNCTION__ + ": WARNING - failed to persist weights for signal index " + IntegerToString(i) + " on shutdown (reason: " + reasonStr + ")");
}
}
g_aiSignalCount = 0;
dbm.Deinit();
Expert.Deinit();
//--- 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.
ClearStatusLabel();
s_deinitInProgress = false;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//--- Expert.OnTimer() does the (comparatively expensive) DB-ranking work, originally paced by its
//--- 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.
#define DB_RANKING_INTERVAL_SECONDS 3600
datetime g_lastDbRankingRun = 0;
//--- Alt-data maintenance (see System\AltDataFetch.mqh): the EA downloads its own missing
//--- history at attach time and keeps appending forward while deployed, so online learning
//--- never depends on an external process. Timer-driven per the OnTimer note below; the
//--- 30-minute check is an in-memory date compare when current - network calls only happen
//--- when a source is genuinely stale (daily for FRED, weekly for COT), and never in the
//--- tester (WebRequest is unavailable there by platform rule; tester reads the cache files).
//+------------------------------------------------------------------+
//| Unknown chart symbol -> ask which instrument it is, once. |
//| Non-blocking: the EA keeps initialising, training and trading |
//| while the dialog sits on the chart. An unmapped symbol just |
//| contributes 0 alt-data features. |
//+------------------------------------------------------------------+
void MaybeAskAltDataMapping(void)
{
if(!EnableAltData || g_altMapAsked || g_altMapDialogOpen)
return;
if(!g_altDataFetch.NeedsMapping(_Symbol))
return; // catalogued, or the user already recorded a choice
g_altMapAsked = true; // one prompt per attach, even if it is closed unanswered
int n = g_altDataFetch.CatalogCount();
string labels[], canon[];
ArrayResize(labels, n);
ArrayResize(canon, n);
for(int i = 0; i < n; i++)
{
labels[i] = g_altDataFetch.CatalogLabel(i);
canon[i] = g_altDataFetch.CatalogName(i);
}
PrintFormat("AltDataFetch: '%s' is not in the alt-data catalog - asking which instrument it maps "
"to. The EA runs normally either way; the answer is saved in symbol_map.cfg.", _Symbol);
if(g_altMapDialog.Show(_Symbol, labels, canon))
g_altMapDialogOpen = true;
else
Print("AltDataFetch: the mapping dialog could not be opened - map it by hand instead: add a "
"line like '" + _Symbol + "=SP500' to Common\\Files\\Warrior_EA\\AltData\\symbol_map.cfg "
"(or '" + _Symbol + "=NONE' to decline).");
}
//+------------------------------------------------------------------+
//| Apply the dialog's answer once the user has clicked. Called from |
//| OnChartEvent, after the dialog has seen the same event. |
//+------------------------------------------------------------------+
//| Drive the FILTERED view's historical reconstruction. |
//| |
//| Re-armed on any era boundary, because that is when the answer |
//| changes: the nets' weights moved and RankTiersFromOos() has just |
//| re-derived every tier's vote weight from that era's holdout, so a |
//| reconstruction from before it describes a model that no longer |
//| exists. Summing the era counters is enough to notice - it only |
//| ever increases, and which member advanced does not matter. |
//| |
//| BARS PER SLICE is small on purpose. Each bar replays Direction() |
//| on every classic filter, which is real indicator work, and this |
//| runs on the chart thread alongside training. An unchunked sweep |
//| here is the 2026-07-26 arrow-restore freeze waiting to happen. |
//+------------------------------------------------------------------+
#define OVERLAY_BARS_PER_SLICE 150
//--- Minimum wall-clock between overlay re-arms. The era-sum trigger alone re-armed far too often
//--- late in a run: at era ~200 the four members complete a barrier round every ~20 seconds, a full
//--- 5,000-bar sweep takes ~17 seconds of slices - so the sweep finished and instantly restarted,
//--- forever, against arrow caches that are half-rebuilt mid-era (the census's "had a voter" count
//--- flapped 1299 -> 257 -> 1113 across three back-to-back sweeps for exactly that reason). Five
//--- minutes keeps the reconstruction current on any human timescale while cutting the churn ~15x.
#define OVERLAY_REARM_MIN_MS 60000
uint g_lastOverlayArmTick = 0;
void AdvanceFilteredSignalOverlay(void)
{
if(DrawUnfilteredSignals)
return; // raw view owns the chart; nothing to reconstruct
//--- ARM ON THE MEMBERS' OWN SIGNAL, not on an era-counter diff. g_warriorOverlayArmRequest is
//--- set by RankTiersFromOos() at pass-3 completion - the moment a member's era-end snapshot
//--- became fresher - which is the only event a redraw can act on. The old era-sum comparison
//--- fired at era BOUNDARIES, i.e. the moment the caches were about to be wiped, and the sweep
//--- then ran against half-built data (the measured 1299 -> 257 -> 1113 voter flap).
//--- Classic-only charts (no AI members) never set the flag: arm once at start instead - the
//--- classic replay is deterministic, so once is also all it needs until a weights refresh.
bool wantArm = g_warriorOverlayArmRequest || (g_aiSignalCount == 0 && g_lastOverlayArmTick == 0);
//--- Re-arm only between sweeps (mid-flight restart would strand the tail undrawn), and at most
//--- once a minute - members finish eras seconds apart, and the limit collapses that burst into
//--- one sweep over everyone's fresh snapshots.
if(wantArm && !Expert.FilteredOverlayPending()
&& (g_lastOverlayArmTick == 0 || GetTickCount() - g_lastOverlayArmTick >= OVERLAY_REARM_MIN_MS))
{
g_warriorOverlayArmRequest = false;
g_lastOverlayArmTick = GetTickCount();
Expert.ArmFilteredOverlay();
}
Expert.AdvanceFilteredOverlay(OVERLAY_BARS_PER_SLICE);
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
void PollAltDataMapDialog(void)
{
if(!g_altMapDialogOpen || !g_altMapDialog.Done())
return;
string choice = g_altMapDialog.Result();
g_altDataFetch.SaveUserMapping(_Symbol, choice);
g_altMapDialog.Destroy(REASON_REMOVE);
g_altMapDialogOpen = false;
if(choice == "NONE")
{
Print("AltDataFetch: recorded 'no alternative data' for " + _Symbol + " - it will not ask "
"again. Remove that line from symbol_map.cfg to be asked at the next attach.");
return;
}
//--- Download now rather than waiting up to 30 minutes for the next upkeep tick.
g_lastAltDataRun = TimeCurrent();
if(g_altDataFetch.Update(_Symbol))
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].AltDataReload();
Print("AltDataFetch: alt data for " + _Symbol + " is being maintained now. A model that was "
"already created without these features keeps its pinned input width - re-attach the EA "
"to build models that actually train on them.");
}
void OnTimer()
{
//--- STOP FIRST. MetaTrader's ~4,500 ms teardown budget starts at the stop REQUEST, and OnDeinit cannot
//--- begin until whatever is in flight returns - so every millisecond spent in a handler after _StopFlag
//--- is raised comes straight out of the chart cleanup, which is what leaves the panel, the status label
//--- and the arrows behind. Nothing below this line matters to a program that is being unloaded: the
//--- training poll, the alt-data upkeep (blocking WebRequests) and the DB ranking pass are all work whose
//--- results are about to be discarded.
if(IsStopped())
return;
//--- 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();
//--- 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();
//--- FILTERED VIEW: keep the reconstructed history in step with the models. One slice per timer
//--- tick, same cadence and same reasoning as the chunked signal rescan above it.
AdvanceFilteredSignalOverlay();
//--- Keep the vote readout tracking the models at timer cadence - Direction() only runs on
//--- new-bar ticks (stock CExpert::Refresh gates it), which on H4 is once every four hours.
Expert.RefreshVoteReadout();
//--- 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();
//--- 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();
}
//--- Alt-data upkeep, before the UseDatabaseRanking early-return so it runs regardless of
//--- that input. First pass backfills any missing history (one blocking WebRequest per stale
//--- source, seconds); steady state is two date compares every 30 minutes.
if(!MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
{
MaybeAskAltDataMapping();
datetime altNow = TimeCurrent();
if(g_lastAltDataRun == 0 || altNow - g_lastAltDataRun >= ALTDATA_CHECK_SECONDS)
{
g_lastAltDataRun = altNow;
if(g_altDataFetch.Update(_Symbol))
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].AltDataReload();
}
}
if(!UseDatabaseRanking)
return;
datetime now = TimeCurrent();
//--- One-shot bypass of the hourly throttle below (see g_forcePatternWeightsRefresh's declaration
//--- comment): a model that just finished its backfill has real DB history sitting unranked, and
//--- waiting up to an hour for it to reach UpdateSignalsWeights() is exactly the "not ready to trade
//--- the instant training finishes" gap this whole feature exists to close.
bool forceNow = g_forcePatternWeightsRefresh;
if(forceNow)
g_forcePatternWeightsRefresh = false;
if(!forceNow && g_lastDbRankingRun != 0 && now - g_lastDbRankingRun < DB_RANKING_INTERVAL_SECONDS)
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)
{
//--- 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;
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)
return;
g_lastAutosaveBarTime = lastBarDate;
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].SaveWeightsNow();
}
void OnTick()
{
#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
//--- STOP FIRST - same reasoning as OnTimer's guard. AutosaveWeightsIfDue() below can write two full nets
//--- per signal on a new bar, and Expert.OnTick() runs the whole trading path; neither is work worth
//--- taking out of the teardown budget. Deliberately AHEAD of the risk-budget update too: a program that
//--- is unloading places no orders, so there is nothing left for the budget to protect.
if(IsStopped())
return;
CheckAlgoTradingState();
//--- 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();
AutosaveWeightsIfDue();
Expert.OnTick();
//--- 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();
//--- 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)
{
//--- STOP FIRST, and this handler matters more than the other two: training is driven by a CUSTOM CHART
//--- EVENT (see CExpertSignalAIBase::OnChartEventHandler -> TuneIndicatorsAndTrain), so an event already
//--- queued when the stop request lands would start a full era-0 warm-up - the MI suite, the geometry
//--- scan, a relabel - inside the teardown window. Every scan now yields on ShutdownRequested() too;
//--- this stops the chain being entered at all.
if(IsStopped())
return;
//--- 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());
//--- The alt-data mapping dialog, when open, drives its own hit-testing the same way.
if(g_altMapDialogOpen)
{
g_altMapDialog.ChartEvent(id, lparam, dparam, sparam);
PollAltDataMapDialog();
}
Expert.OnChartEvent(id, lparam, dparam, sparam);
//--- 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();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
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);
//--- ATR unit lookback, pinned - decoupled from the derived input window, see Inputs.mqh.
signal_obj.Periods(ATR_FEATURE_PERIOD);
signal_obj.SLMode((int)SL_Mode);
signal_obj.TPMode((int)TP_Mode);
signal_obj.ConfidenceSource((int)Confidence_Source);
//--- 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);
//--- Pattern-table row cap; raised via the input for meta-label corpus builds (design doc S1).
signal_obj.MaxTableRows(DB_MaxRowsPerTable);
//--- 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
//--- 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.
signal_obj.ThresholdOpen((int)Min_Vote_Open);
signal_obj.ThresholdClose((int)Min_Vote_Close);
//--- The hold-to-barrier exit policy went with the fractal target (withdrawn - see Inputs.mqh). It
//--- existed because the fractal vote flips at swing-marker cadence (~3-5 bars), far inside the
//--- barrier's travel time, so vote-driven closes would have cut trades the gate had certified as
//--- hold-to-barrier. Barrier-target models keep the vote exits and always did: their label IS the
//--- vote's own horizon. CExpertSignalCustom::m_holdToBarrier and HoldToBarrier() remain, unset here.
//--- HYBRID is now one fused signal, so no separate AI quorum is needed here.
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;
}
}
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;
}
}
// 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);
//--- 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);
money.ConfidenceSource((int)Confidence_Source);
}
// Add more money management strategies if needed
return true;
}
//+------------------------------------------------------------------+