Warrior_EA/Warrior_EA.mq5
AnimateDread c393497fd6 fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era
Full-pipeline analysis after "threshold 30, attained often, nothing drawn,
still glued to buy". The log falsified the premise before any code did:

  21:40:43  swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0%
  21:42:07  swept 4999, 0 voters,  drew 0
  21:51:30  swept 4999, 0 voters,  drew 0
  21:56:30  swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0%

The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root
cause, three symptoms: every display path read m_arrowSignalCache, which is
wiped to sentinel at each era start and only complete again when pass 3
finishes. With eras at ~30s and a sweep at ~17s:

 * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its
   else-branch deleted the arrow on every voteless bar - erasing the previous
   sweep's entire output. The chart cycled populated -> blank -> populated;
   the user kept catching the blank phase.
 * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every
   era and fell through to dPrevSignal - the frozen purge-band edge bar that
   reads Buy. 659638e fixed which bar was frozen, not the freezing.
 * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a
   different fraction of half-rebuilt caches.

THE FIX, structural rather than another patch:

1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one
   moment the cache is complete - and now copies it (raw signals, newest
   LOOKBACK+16 bars) into member-owned snapshot state, unconditionally,
   BEFORE its early return: an all-Neutral era is a snapshot worth showing,
   not an absence of one. Raw signals rather than votes, so a tier re-rank
   between eras reprices them at read time via LiveVoteContribution for free.
2. The sweep (SnapshotVoteAt) and the prospective readout both read
   snapshots; the readout's fallback chain is live-cache -> snapshot ->
   dPrevSignal, and the snapshot leg is the one that fires most of the time.
3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual
   sub-threshold vote takes an arrow down. This alone ends the wipe half of
   the flicker even where snapshots are missing (before the first era).
4. Arming moved from an era-counter diff (which fires at era BOUNDARIES,
   i.e. precisely when caches are about to be wiped) to
   g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's
   snapshot just got fresher", the only event a redraw can act on. 60s rate
   limit collapses the four members' burst into one sweep. Classic-only
   charts arm once at start.
5. Census now reports the direction split - "922 had a voter (610 buy / 312
   sell)" - so "the vote leans buy" is checkable from the log instead of
   inferred from arrow colours.

Also visible in the log and worth knowing: the threshold flip-flopped
30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51
ran at 40), so part of the observed blankness was configuration, not code.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00

2198 lines
122 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 (per AIType - MLP/CONV/LSTM/HYBRID), so the control |
//| 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 a SINGLE AIType can create at once. That stopped
//--- being 3 when AI_HYBRID became the ensemble preset (609be10): it enables PAI + CONV + LSTM +
//--- CONVLSTM = FOUR, and RegisterAISignal registers them in that order, so at 3 the CONVLSTM member
//--- was silently dropped on the floor for every ensemble run. Sized to 5 so adding META to a preset
//--- cannot reintroduce this; the array holds borrowed pointers, so the two spare slots cost nothing.
#define MAX_AI_SIGNALS 5
//--- Printed at OnInit so tester logs prove which binary is actually running.
#define WARRIOR_BUILD_TAG "reset-census-v6"
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 this AIType enables 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. AIType=All with one signal paused and one running) still shows an actionable label
bool AllTrainingPaused(void)
{
if(g_aiSignalCount == 0)
return false;
for(int i = 0; i < g_aiSignalCount; i++)
if(!g_aiSignals[i].IsTrainingPaused())
return false;
return true;
}
bool AllTrainingStopped(void)
{
if(g_aiSignalCount == 0)
return false;
for(int i = 0; i < g_aiSignalCount; i++)
if(!g_aiSignals[i].IsTrainingStopped())
return false;
return true;
}
//--- "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)
{
for(int i = ObjectsTotal(0, 0, OBJ_ARROW) - 1; i >= 0; i--)
{
string name = ObjectName(0, i, 0, OBJ_ARROW);
ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
}
ChartRedraw(0);
}
void ToggleSignalsVisibility(void)
{
g_signalsVisible = !g_signalsVisible;
//--- 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 says 1 on an AI_HYBRID chart, 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)
}
//+------------------------------------------------------------------+
//| 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",
(int)AIType, (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. META never reaches here with AI_HYBRID (EnableMETA is AI_META-only).
if(AIType == AI_HYBRID)
//--- 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 - AIType=%d Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d TrailingStrategy=%d MM_STRATEGY=%d",
__FUNCTION__, (int)AIType, (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.
const string dbVersion = "3.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);
//+------------------------------------------------------------------+
//| AIType selects which single AI architecture this run trades/ |
//| trains: MLP, CONV, LSTM, or HYBRID. HYBRID is the fused stacked |
//| model, not a 2-of-3 ensemble. |
//| - 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. |
//+------------------------------------------------------------------+
//--- AI_HYBRID is the ENSEMBLE preset: all four direction NNs run on this one chart (see AI_CHOICE's
//--- declaration comment). EnableHYBRID still names the CONVLSTM topology INSTANCE (CSignalHYBRID,
//--- State\HYBRID\) - only the enum was renamed, the class and its files keep their identity.
EnablePAI = (AIType == AI_MLP || AIType == AI_HYBRID);
EnableCONV = (AIType == AI_CONV || AIType == AI_HYBRID);
EnableLSTM = (AIType == AI_LSTM || AIType == AI_HYBRID);
EnableHYBRID = (AIType == AI_CONVLSTM || AIType == AI_HYBRID);
EnableMETA = (AIType == AI_META);
// 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 AIType's files)
g_aiSignalCount = 0;
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);
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;
}
//+------------------------------------------------------------------+