forked from MrBaro75/Warrior_EA
cooldown-recon put the chart-wide cooldown at the end of the overlay sweep. It executed ZERO times. This store's own header already said why: the sweep 're-arms only when an era ends. A DEPLOYED ensemble runs no further eras'. Five of six charts were deployed, so there were zero 'Filtered view: swept' lines in the entire session while the saved files still held 148 same-side pairs under 30 bars on XTIUSD. Moved to the completion of the progressive vote-arrow restore, which runs on every chart including deployed ones. The restore thinning alone was never going to be enough either: MT5 persists chart objects in profiles\Charts\*\chart*.chr, so arrows drawn under an older window are ALREADY on the chart when the process starts, and a freshly-thinned restore just adds to them. Two correctly-thinned sets still union into clusters. The chart is the only authority. Same construction as before: OBJ_TREND only (the line is the canonical half of a mark, matching Snapshot()), sorted by time first because object order is not time order, and the gap>0 guard so a mis-ordered set fails visibly by keeping rather than silently by deleting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2200 lines
118 KiB
MQL5
2200 lines
118 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
//--- THIS IS THE PRIVATE / PROP-FIRM BUILD, AND THE ONLY ONE. The MQL5 Market variant is gone
|
|
//--- (2026-08-23): Market rule IV forbids DLL calls, and the DLL compute tier plus the WebRequest
|
|
//--- alt-data fetch are what make this bot work at all. If it is ever sold it goes through its own
|
|
//--- channel with the DLLs intact, so there is nothing left for a no-DLL build to be for. Custom
|
|
//--- indicators load by bare name from <MQL5>\Indicators\; nothing is embedded as a #resource.
|
|
//--- 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.
|
|
#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"
|
|
//--- Persistence for the COMBINED-VOTE arrow layer - the one the chart actually shows while
|
|
//--- DrawUnfilteredSignals is off. Must come after ExpertCustom.mqh: it builds on SIG_VOTE_PREFIX
|
|
//--- and WarriorPlotSignalLevel, which ExpertSignalCustom.mqh defines.
|
|
#include "Expert\Chart\VoteArrows.mqh"
|
|
#include "System\Random.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) are |
|
|
//| loaded via CiCustom/IND_CUSTOM (see ExpertSignalAIBase.mqh). |
|
|
//+------------------------------------------------------------------+
|
|
//
|
|
CExpertCustom Expert;
|
|
CDatabaseManager dbm();
|
|
CTradeJournalManager journal;
|
|
//+------------------------------------------------------------------+
|
|
//| Pointers to whichever AI signal instances this run actually |
|
|
//| created (any enabled Use_* subset, plus the meta head), so the |
|
|
//| panel can drive training/weight actions on exactly the signal(s) |
|
|
//| in play this run and never touch another config's files. |
|
|
//+------------------------------------------------------------------+
|
|
//--- Must be >= the number of AI signal instances one run can create at once. At 3 the CONVLSTM
|
|
//--- member was once silently dropped on the floor (609be10); the array holds borrowed pointers, so
|
|
//--- headroom is free - but a cap that silently discards a model is a trapdoor, hence the loud
|
|
//--- refusal below.
|
|
#define MAX_AI_SIGNALS 5
|
|
//--- Printed at OnInit so tester logs prove which binary is actually running.
|
|
#define WARRIOR_BUILD_TAG "cooldown-recon2"
|
|
CExpertSignalAIBase *g_aiSignals[MAX_AI_SIGNALS];
|
|
int g_aiSignalCount = 0;
|
|
void RegisterAISignal(CExpertSignalAIBase *sig)
|
|
{
|
|
if(sig == NULL)
|
|
return;
|
|
//--- LOUD on overflow. A cap that silently discards a model is not a guard, it is a trapdoor.
|
|
if(g_aiSignalCount >= MAX_AI_SIGNALS)
|
|
{
|
|
Print(__FUNCTION__ + ": CANNOT REGISTER a further AI signal - MAX_AI_SIGNALS is " +
|
|
IntegerToString(MAX_AI_SIGNALS) + " and this run already created that many. The extra "
|
|
"model would still train and still vote, but the control panel could not reach it and its "
|
|
"weights would never be autosaved or flushed on shutdown. Raise MAX_AI_SIGNALS to at least "
|
|
"the number of instances the enabled Use_* inputs create and recompile.");
|
|
return;
|
|
}
|
|
g_aiSignals[g_aiSignalCount++] = sig;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Control panel: a CAppDialog-based GUI (see Panel\ControlPanel.mqh) |
|
|
//| with show/hide signals, start/pause/stop training, and save/load/ |
|
|
//| delete-reset weights buttons for the currently-active AI |
|
|
//| signal(s) only. The dialog's own caption bar provides the show/ |
|
|
//| hide (minimize) control - no separate toggle button needed. |
|
|
//+------------------------------------------------------------------+
|
|
//--- default spawn position: top-right corner, clear of the status label text block (top-left) so the
|
|
//--- two don't overlap on first run - the panel is fully draggable afterwards via its caption bar,
|
|
//--- so this is only a starting point, not a constraint.
|
|
#define CP_Y0 10
|
|
#define CP_RIGHT_MARGIN 80
|
|
CControlPanel ExtPanel;
|
|
//--- Alt-data maintenance + the one-time symbol-mapping dialog. Declared HERE rather than beside
|
|
//--- OnTimer because OnDeinit (further up the file) tears the dialog down, and MQL5 resolves global
|
|
//--- variables in declaration order.
|
|
#define ALTDATA_CHECK_SECONDS 1800
|
|
datetime g_lastAltDataRun = 0;
|
|
CAltDataFetch g_altDataFetch;
|
|
CAltDataMapDialog g_altMapDialog;
|
|
bool g_altMapDialogOpen = false;
|
|
bool g_altMapAsked = false; // one prompt per attach, even if the user closes it unanswered
|
|
bool g_signalsVisible = true;
|
|
#define SIGNAL_VISIBILITY_STATE_SUFFIX ".sigvis"
|
|
//--- EVERY WARRIOR FILE LIVES UNDER Warrior_EA\, AND TWO OF THEM DID NOT (fixed 2026-08-26).
|
|
//--- The .sigvis and .votearrows sidecars were written to the ROOT of Common\Files, outside the
|
|
//--- one directory that "wipe the Warrior EA files" has always meant. Two consecutive wipes
|
|
//--- therefore left them standing, and the second fresh start restored 115-431 combined-vote arrows
|
|
//--- per chart onto models training from era 0. A wipe that does not remove all of a program's
|
|
//--- state is not a wipe, and the operator has no way to know which files were missed.
|
|
#define WARRIOR_STATE_DIR "Warrior_EA\\"
|
|
string SignalsVisibilityStateFile(void)
|
|
{
|
|
return WARRIOR_STATE_DIR + "ChartState\\" + 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.
|
|
datetime g_lastAutosaveBarTime = 0;
|
|
//--- TESTER PASS SELF-PROFILE. Three coarse buckets accumulated per tick (plus the timer's own),
|
|
//--- printed once at the pass's OnDeinit - so a slow pass NAMES its own consumer instead of being
|
|
//--- guessed at from the outside (the 2026-08-25 "0.1% an hour" report took a day of guessing that
|
|
//--- one PrintFormat would have answered). Two clock reads per tick when active, zero when live.
|
|
bool g_tpActive = false;
|
|
long g_tpTicks = 0, g_tpTimers = 0;
|
|
ulong g_tpPreUs = 0, g_tpExpertUs = 0, g_tpJournalUs = 0, g_tpTimerUs = 0;
|
|
//--- last observed AllTrainingDeployed() value, so OnTimer() can spot training deploying itself (plateau
|
|
//--- ladder / era cap) and resync the panel's button labels exactly once on the transition
|
|
bool g_lastDeployedState = false;
|
|
//--- summarizes state across all currently-active AI signals for button labels;
|
|
//--- "paused"/"stopped" only report true if EVERY active signal agrees, so a mixed state
|
|
//--- (e.g. several NNs enabled with one paused and one running) still shows an actionable label
|
|
//--- Counted over the SIGNAL TREE, not over g_aiSignals[]: the tree is the population a panel command
|
|
//--- actually reaches, so a label can no longer describe a different set of models than the button acts
|
|
//--- on. SIGTRAIT_TRAINABLE is the denominator - "all paused" means nothing without it.
|
|
int TrainableSignalCount(void) { return Expert.CountSignalTrait(SIGTRAIT_TRAINABLE); }
|
|
bool AllTrainingPaused(void)
|
|
{
|
|
int n = TrainableSignalCount();
|
|
return n > 0 && Expert.CountSignalTrait(SIGTRAIT_TRAINING_PAUSED) == n;
|
|
}
|
|
bool AllTrainingStopped(void)
|
|
{
|
|
int n = TrainableSignalCount();
|
|
return n > 0 && Expert.CountSignalTrait(SIGTRAIT_TRAINING_STOPPED) == n;
|
|
}
|
|
//--- "deployed" = every active signal has finalised a model and is running live inference rather
|
|
//--- than training.
|
|
bool AllTrainingDeployed(void)
|
|
{
|
|
int n = TrainableSignalCount();
|
|
return n > 0 && Expert.CountSignalTrait(SIGTRAIT_TRAINING_COMPLETE) == n;
|
|
}
|
|
//--- 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)
|
|
{
|
|
return Expert.CountSignalTrait(SIGTRAIT_DEPLOY_SKIPS_RECALL) > 0;
|
|
}
|
|
void ApplySignalsVisibility(void)
|
|
{
|
|
//--- TYPED-BLIND, PREFIX-SCOPED. A mark is a line AND an arrow (see WarriorPlotSignalLevel), so
|
|
//--- a type-filtered sweep would toggle half of each one and leave the chart showing arrows for
|
|
//--- signals whose levels are hidden.
|
|
for(int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--)
|
|
{
|
|
string name = ObjectName(0, i, -1, -1);
|
|
if(StringFind(name, SIG_ARROW_PREFIX) != 0)
|
|
continue;
|
|
ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
|
|
}
|
|
ChartRedraw(0);
|
|
}
|
|
void ToggleSignalsVisibility(void)
|
|
{
|
|
g_signalsVisible = !g_signalsVisible;
|
|
//--- Hide->Show is also the operator's manual "these arrows look stale" refresh: rescan each
|
|
//--- deployed model against recent history BEFORE re-showing, so Show Signals reveals a fresh
|
|
//--- set instead of just re-exposing whatever old render the .arrows sidecar happened to hold
|
|
//--- (see CExpertSignalAIBase::StartChartSignalRescan/AdvanceChartSignalRescan).
|
|
if(g_signalsVisible)
|
|
{
|
|
bool anyQueued = (Expert.DispatchSignalCommand(SIGCMD_RESCAN_SIGNALS) > 0);
|
|
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;
|
|
if(Expert.CountSignalTrait(SIGTRAIT_RESCAN_PENDING) > 0)
|
|
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 = (TrainableSignalCount() == 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.
|
|
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).
|
|
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.
|
|
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;
|
|
}
|
|
//--- One handler per CP_ACTION_* below (called only from HandleControlPanelAction's dispatch table
|
|
//--- at the bottom of this block) - each keeps its own guard/confirm/Alert sequence exactly as it was
|
|
//--- when all nine lived in one switch, just independently readable/testable now.
|
|
void HandleCpToggleSignals(void)
|
|
{
|
|
ToggleSignalsVisibility();
|
|
//--- If a rescan got queued (Show Signals with a deployed model to re-infer from), the
|
|
//--- label refresh + Alert are deferred to FinalizeSignalsRescanIfDone() - firing "shown"
|
|
//--- here would lie about arrows that don't exist on the chart yet.
|
|
if(!g_signalsRescanPending)
|
|
{
|
|
RefreshControlPanelLabels();
|
|
Alert("Warrior EA: signal arrows " + (g_signalsVisible ? "shown" : "hidden"));
|
|
}
|
|
}
|
|
void HandleCpTogglePause(void)
|
|
{
|
|
//--- A deployed model has no training run to pause - say so instead of silently doing nothing.
|
|
if(AllTrainingDeployed())
|
|
{
|
|
Alert("Warrior EA: the model is deployed - there is no training run to pause.\nUse \"Retrain Model\" first if you want to train it further.");
|
|
return;
|
|
}
|
|
//--- Direction resolved ONCE here, then handed to every signal as a plain command. A toggle
|
|
//--- each model re-derived from its own state is how a mixed set ends up half paused.
|
|
bool pause = !AllTrainingPaused();
|
|
int touched = Expert.DispatchSignalCommand(pause ? SIGCMD_PAUSE_TRAINING : SIGCMD_RESUME_TRAINING);
|
|
RefreshControlPanelLabels();
|
|
Alert("Warrior EA: training " + (pause ? "paused" : "resumed") +
|
|
" (" + IntegerToString(touched) + " model(s))");
|
|
}
|
|
void HandleCpToggleStop(void)
|
|
{
|
|
//--- Same as Pause: Stop/Start operate on a training run, and a deployed model isn't one.
|
|
//--- Routing this to RetrainDeployed() instead would silently do the Deploy button's job.
|
|
if(AllTrainingDeployed())
|
|
{
|
|
Alert("Warrior EA: the model is deployed and already running live inference, not training.\nUse \"Retrain Model\" to put it back into training.");
|
|
return;
|
|
}
|
|
bool doStop = !AllTrainingStopped();
|
|
int touched = Expert.DispatchSignalCommand(doStop ? SIGCMD_STOP_TRAINING : SIGCMD_START_TRAINING);
|
|
RefreshControlPanelLabels();
|
|
Alert("Warrior EA: training " + (doStop ? "stopped" : "restarted") +
|
|
" (" + IntegerToString(touched) + " model(s))");
|
|
}
|
|
void HandleCpToggleDeploy(void)
|
|
{
|
|
if(TrainableSignalCount() == 0)
|
|
{
|
|
Alert("Warrior EA: no AI signal is active - nothing to deploy (set the AI algorithm input to something other than Disabled).");
|
|
return;
|
|
}
|
|
//--- RETRAIN direction: put the finalised model back into training, continuing from its own
|
|
//--- weights. Reversible, non-destructive (the weights on disk stay), so no confirmation.
|
|
if(AllTrainingDeployed())
|
|
{
|
|
Expert.DispatchSignalCommand(SIGCMD_RETRAIN_DEPLOYED);
|
|
RefreshControlPanelLabels();
|
|
Alert("Warrior EA: retraining the deployed model - it continues from its current weights.\nUse \"Delete & Reset Weights\" instead to start from scratch.");
|
|
return;
|
|
}
|
|
//--- DEPLOY direction. The plateau ladder refuses to auto-deploy a model that never cleared the
|
|
//--- per-class recall floor (it would be one that ignores Buy or Sell); a manual deploy is the
|
|
//--- operator's call, but they should make it knowingly - so this is the one case that asks.
|
|
if(AnyDeployWouldSkipRecallFloor() &&
|
|
!ConfirmDestructiveAction("No training era has cleared the per-class recall floor yet, so this model may "
|
|
"be ignoring Buy or Sell entirely.\n\nDeploy it anyway as the final model?"))
|
|
{
|
|
Alert("Warrior EA: deploy cancelled - training continues");
|
|
return;
|
|
}
|
|
int deployed = Expert.DispatchSignalCommand(SIGCMD_DEPLOY);
|
|
RefreshControlPanelLabels();
|
|
if(deployed == 0)
|
|
Alert("Warrior EA: could not deploy - the AI signal is not initialised yet.");
|
|
else
|
|
Alert("Warrior EA: model deployed (" + IntegerToString(deployed) + " signal(s)).\nTraining stopped; it now runs live inference. Click \"Retrain Model\" to train it further.");
|
|
}
|
|
void HandleCpSave(void)
|
|
{
|
|
int saved = Expert.DispatchSignalCommand(SIGCMD_SAVE_WEIGHTS);
|
|
Alert("Warrior EA: weights saved (" + IntegerToString(saved) + " of " +
|
|
IntegerToString(TrainableSignalCount()) + " model(s))");
|
|
}
|
|
void HandleCpLoad(void)
|
|
{
|
|
Expert.DispatchSignalCommand(SIGCMD_LOAD_WEIGHTS);
|
|
//--- A reload carries the saved file's own "deployed" flag, so it can flip the whole training
|
|
//--- state machine (loading a finalised model makes Deploy read "Retrain Model", and Pause/Stop
|
|
//--- read "(deployed)"). Resync the labels or the panel would keep offering stale actions.
|
|
RefreshControlPanelLabels();
|
|
Alert("Warrior EA: weights reloaded from disk");
|
|
}
|
|
void HandleCpReset(void)
|
|
{
|
|
//--- CENSUS BEFORE THE ACTION, walked over the SAME tree the reset itself walks - so the
|
|
//--- count and the identity lines describe exactly the models about to be wiped and cannot
|
|
//--- drift from them.
|
|
int trainable = TrainableSignalCount();
|
|
PrintFormat("%s: RESET requested - %d AI signal(s) reachable on this chart:",
|
|
__FUNCTION__, trainable);
|
|
Expert.DispatchSignalCommand(SIGCMD_REPORT_IDENTITY);
|
|
if(!ConfirmDestructiveAction("Delete the saved AI weights of all " +
|
|
IntegerToString(trainable) +
|
|
" AI model(s) on this chart and restart training from era 0?"))
|
|
{
|
|
Alert("Warrior EA: weights reset cancelled");
|
|
return;
|
|
}
|
|
//--- ResetWeights() returns whether the fresh topology was REBUILT, and that return has
|
|
//--- been discarded since it was written. Count it, and say so when they disagree.
|
|
int resetOk = Expert.DispatchSignalCommand(SIGCMD_RESET_WEIGHTS);
|
|
RefreshControlPanelLabels();
|
|
PrintFormat("%s: RESET complete - %d of %d AI signal(s) rebuilt a fresh topology.",
|
|
__FUNCTION__, resetOk, trainable);
|
|
if(trainable == 0)
|
|
Alert("Warrior EA: nothing to reset - no AI signal is registered on this chart.");
|
|
else
|
|
if(resetOk == trainable)
|
|
Alert("Warrior EA: weights reset for " + IntegerToString(resetOk) +
|
|
" model(s) - training restarts from era 0");
|
|
else
|
|
Alert("Warrior EA: weights reset INCOMPLETE - " + IntegerToString(resetOk) + " of " +
|
|
IntegerToString(trainable) + " model(s) rebuilt.\nThe rest had their files "
|
|
"deleted but could not rebuild a topology - see the Experts log.");
|
|
}
|
|
void HandleCpReport(void)
|
|
{
|
|
if(!UseDatabaseRanking)
|
|
{
|
|
Print(__FUNCTION__ + ": trade journal report requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
|
|
Alert("Warrior EA: trade journal report requires \"Weight filters by DB win-rate\" to be enabled");
|
|
return;
|
|
}
|
|
string reportPath, reportError;
|
|
if(journal.GenerateReport(reportPath, reportError))
|
|
{
|
|
Print(__FUNCTION__ + ": trade journal report ready - " + reportPath);
|
|
Alert("Warrior EA: trade journal report exported - see the Experts log for the file path");
|
|
}
|
|
else
|
|
{
|
|
Print(__FUNCTION__ + ": could not generate trade journal report - " + reportError);
|
|
Alert("Warrior EA: could not export trade journal report - " + reportError);
|
|
}
|
|
}
|
|
//--- separate from HandleCpReset on purpose: resetting AI weights (a routine, frequent action while
|
|
//--- tuning) must never cost the trader their accumulated pattern-confidence/trade-journal history,
|
|
//--- and vice versa - these are two independent "start fresh" decisions.
|
|
void HandleCpResetDb(void)
|
|
{
|
|
if(UseDatabaseRanking)
|
|
{
|
|
if(!ConfirmDestructiveAction("Delete the trade-journal and pattern-confidence database?"))
|
|
{
|
|
Alert("Warrior EA: database reset cancelled");
|
|
return;
|
|
}
|
|
//--- ResetDatabase() returns false when the file could not be deleted or the connection
|
|
//--- could not be reopened, and that return was discarded too - the Alert said
|
|
//--- "database reset" either way.
|
|
bool dbReset = dbm.ResetDatabase();
|
|
PrintFormat("%s: DATABASE RESET %s - one shared trade-journal/pattern-confidence DB serves all"
|
|
" %d AI signal(s) on this chart; per-model weights are NOT affected (use Delete &&"
|
|
" Reset Weights for those).", __FUNCTION__,
|
|
(dbReset ? "succeeded" : "FAILED"), TrainableSignalCount());
|
|
Alert(dbReset
|
|
? "Warrior EA: database reset"
|
|
: "Warrior EA: database reset FAILED - see the Experts log");
|
|
}
|
|
else
|
|
{
|
|
Print(__FUNCTION__ + ": database reset requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
|
|
Alert("Warrior EA: database reset requires \"Weight filters by DB win-rate\" to be enabled");
|
|
}
|
|
}
|
|
//--- 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: HandleCpToggleSignals(); break;
|
|
case CP_ACTION_TOGGLE_PAUSE: HandleCpTogglePause(); break;
|
|
case CP_ACTION_TOGGLE_STOP: HandleCpToggleStop(); break;
|
|
case CP_ACTION_TOGGLE_DEPLOY: HandleCpToggleDeploy(); break;
|
|
case CP_ACTION_SAVE: HandleCpSave(); break;
|
|
case CP_ACTION_LOAD: HandleCpLoad(); break;
|
|
case CP_ACTION_RESET: HandleCpReset(); break;
|
|
case CP_ACTION_REPORT: HandleCpReport(); break;
|
|
case CP_ACTION_RESET_DB: HandleCpResetDb(); break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
// Helper function to pause execution for a random duration between 1 to 3 seconds
|
|
void RandomSleep()
|
|
{
|
|
Sleep(WarriorRandInt(2000) + 1000); // Sleeps between 1000ms (1s) and 3000ms (3s)
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| The DB fingerprint's first slot - was (int)AIType until the |
|
|
//| preset selector was replaced by the per-NN toggles (2026-08-19). |
|
|
//+------------------------------------------------------------------+
|
|
int DbLegacyAiSlot()
|
|
{
|
|
int bits = (Use_MLP ? 1 : 0) | (Use_CONV ? 2 : 0) | (Use_LSTM ? 4 : 0) | (Use_CONVLSTM ? 8 : 0);
|
|
if(bits == 15)
|
|
return 6; // all four = the old AI_HYBRID ensemble preset
|
|
if(bits == 0)
|
|
return 0; // legacy AI_NONE (slot 5 was AI_META, removed 2026-08-25)
|
|
if(bits == 1)
|
|
return 1; // AI_MLP
|
|
if(bits == 2)
|
|
return 2; // AI_CONV
|
|
if(bits == 4)
|
|
return 3; // AI_LSTM
|
|
if(bits == 8)
|
|
return 4; // AI_CONVLSTM
|
|
return 100 + bits; // new subset - outside the legacy value space by construction
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Config fingerprint appended to the pattern-confidence/trade- |
|
|
//| journal database's filename, so a topology or feature-set change |
|
|
//| that would produce a differently-shaped/behaving model gets its |
|
|
//| own database instead of silently reusing pattern-weight/journal |
|
|
//| history that no longer matches what's actually trading now. |
|
|
//+------------------------------------------------------------------+
|
|
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.
|
|
string fp = StringFormat("SEM%d|", SIGNAL_DB_SEMANTICS_VERSION)
|
|
+ StringFormat("%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d",
|
|
DbLegacyAiSlot(), (int)OutputNeuronsCount, (int)TrainingOptimizer,
|
|
//--- LEGACY SLOT (was ind_Periods, derived since 2026-08-11). The literal
|
|
//--- is the shipped default so every existing database keeps its key.
|
|
20,
|
|
EnableVolume, EnableTime, EnableATR,
|
|
//--- LEGACY VOTE SLOTS (were EnableMA / EnableRSI, removed 2026-08-24).
|
|
//--- Both shipped false and were hashed UNCONDITIONALLY, so every existing
|
|
//--- database is keyed on the 0 they contributed; the literals keep those
|
|
//--- keys intact. g_TunedMaPeriod/g_TunedMaType stay live - the MA FEATURE
|
|
//--- still runs at the adopted periods, and the DB key must describe those
|
|
//--- (new periods = new pattern definitions = fresh win-rate history).
|
|
0, g_TunedMaPeriod, g_TunedMaType, 0, g_TunedRsiPeriod,
|
|
EnableSwingContext, EnableNews,
|
|
//--- LEGACY FEATURE SLOTS (were the five AD/Wyckoff flags, removed
|
|
//--- 2026-08-24). Hashed unconditionally and all shipped false, so the
|
|
//--- literals below are exactly what every existing database is keyed on.
|
|
0, 0, 0)
|
|
+ StringFormat("|%d|%d|%d|%d", 0, 0,
|
|
EnableMAFeature, 0); //--- last 0 = former EnableRSIFeature
|
|
//--- The MACD and Ichimoku segments were appended ONLY WHEN ENABLED and both flags are now gone, so
|
|
//--- they can never appear - which is byte-identical to every fingerprint this has ever produced,
|
|
//--- since neither feature has shipped enabled. Nothing is orphaned by their removal.
|
|
//--- 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. 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 every trade-management enum input actually holds a |
|
|
//| member of its own enum. See the call site in OnInit() for why |
|
|
//| this is a hard gate and not a clamp. Returns false (and explains |
|
|
//| itself) on any stale value. |
|
|
//| |
|
|
//| WIDENED 2026-08-25, and this is the reason it had to be: removing |
|
|
//| the five confidence-scaled trade-management modes vacated a value |
|
|
//| in FOUR enums at once, and MetaTrader validates none of them when |
|
|
//| it replays a saved .set or a stored optimization pass. Left |
|
|
//| unguarded, a chart saved with the Intelligent stop would have fed |
|
|
//| SL_Mode = -1 into a slMultiplier that is now used verbatim - a |
|
|
//| stop placed on the WRONG SIDE of the entry, silently, on a live |
|
|
//| account. Refusing to start is the only acceptable response to an |
|
|
//| input whose meaning changed underneath a saved file. |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateTradeManagementInputs()
|
|
{
|
|
int sl = (int)SL_Mode;
|
|
bool slOk = (sl == SL_ATR_x1 || sl == SL_ATR_x2 || sl == SL_ATR_x3);
|
|
int tp = (int)TP_Mode;
|
|
bool tpOk = (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);
|
|
int en = (int)Entry_Multiplier;
|
|
bool enOk = (en == MARKET || en == LIMIT_1xATR || en == LIMIT_2xATR || en == LIMIT_3xATR ||
|
|
en == STOP_1xATR || en == STOP_2xATR || en == STOP_3xATR);
|
|
int tr = (int)TrailingStrategy;
|
|
bool trOk = (tr == TRAILING_STRATEGY_NONE || tr == TRAILING_STRATEGY_ATR_x1 ||
|
|
tr == TRAILING_STRATEGY_ATR_x2 || tr == TRAILING_STRATEGY_ATR_x3);
|
|
int mm = (int)MM_STRATEGY;
|
|
bool mmOk = (mm == FIXED_RISK || mm == FIXED_LOT);
|
|
if(slOk && tpOk && enOk && trOk && mmOk)
|
|
return true;
|
|
string bad = "";
|
|
if(!slOk)
|
|
bad += (bad == "" ? "" : ", ") + "Stop-loss mode (" + IntegerToString(sl) + ")";
|
|
if(!tpOk)
|
|
bad += (bad == "" ? "" : ", ") + "Take-profit mode (" + IntegerToString(tp) + ")";
|
|
if(!enOk)
|
|
bad += (bad == "" ? "" : ", ") + "Entry type/offset (" + IntegerToString(en) + ")";
|
|
if(!trOk)
|
|
bad += (bad == "" ? "" : ", ") + "Trailing stop (" + IntegerToString(tr) + ")";
|
|
if(!mmOk)
|
|
bad += (bad == "" ? "" : ", ") + "MM strategy (" + IntegerToString(mm) + ")";
|
|
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 the option(s) named above from their "
|
|
"dropdowns, then press OK.");
|
|
Alert("Warrior EA: " + bad + " is invalid - re-pick it in the Inputs tab. See the Experts log.");
|
|
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 ValidateTradeManagementInputs(): refuse |
|
|
//| 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. |
|
|
//+------------------------------------------------------------------+
|
|
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.SignalClusterWindow(WarriorSignalCooldownBars());
|
|
aiSignal.SignalCooldownScope(Signal_CooldownScope);
|
|
aiSignal.FreezePriorCalibration(FreezePriorCalibration);
|
|
aiSignal.SwingConfirmationBars(SwingConfirmationBars);
|
|
if((EnablePAI ? 1 : 0) + (EnableCONV ? 1 : 0) + (EnableLSTM ? 1 : 0) + (EnableHYBRID ? 1 : 0) >= 2)
|
|
//--- second argument mirrors the open threshold into the combined-vote OOS scorer so the
|
|
//--- ensemble panel's "Ensemble vote" line fires on the same criterion the live trade does
|
|
aiSignal.EnsembleMember(true, (double)Signal_ThresholdOpen);
|
|
aiSignal.EnableOnlineLearning(EnableOnlineLearning);
|
|
aiSignal.MaxErasPerRun(MaxErasPerRun);
|
|
aiSignal.OOSSplit(OOSSplit);
|
|
if(!UseDatabaseRanking)
|
|
aiSignal.Weight(1);
|
|
aiSignal.UseVolumes(EnableVolume);
|
|
aiSignal.UseTime(EnableTime);
|
|
aiSignal.UseATR(EnableATR);
|
|
aiSignal.UseMA(EnableMAFeature);
|
|
aiSignal.UseSwingContext(EnableSwingContext);
|
|
aiSignal.UseNews(EnableNews);
|
|
aiSignal.NewsFeatureWindowMinutes(NewsFeatureWindowMinutes);
|
|
aiSignal.UseCrossAsset(EnableCrossAsset);
|
|
aiSignal.UseSpreadFeature(EnableSpreadFeature);
|
|
aiSignal.UseAltData(EnableAltData);
|
|
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;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
// Helper: new + null-check + Expert.InitMoney() + null-check, shared by
|
|
// every MM_STRATEGY branch in InitializeMoneyManagement().
|
|
template <typename TMoney>
|
|
TMoney* CreateAndInitMoney(const string functionName)
|
|
{
|
|
TMoney *money = new TMoney;
|
|
if(money == NULL)
|
|
{
|
|
Print(functionName + ": error creating money");
|
|
return NULL;
|
|
}
|
|
if(!Expert.InitMoney(money))
|
|
{
|
|
Print(functionName + ": error initializing money");
|
|
return NULL;
|
|
}
|
|
return money;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| ONE RETRY-AND-REPORT for the init steps that can lose a race |
|
|
//| with the terminal (indicator handles, timer registration, the |
|
|
//| trade objects' own setup). |
|
|
//+------------------------------------------------------------------+
|
|
typedef bool (*TInitStep)(void);
|
|
|
|
bool RetryInitStep(TInitStep step, const string what, const int maxRetries, const string caller)
|
|
{
|
|
for(int tries = 0; tries < maxRetries; ++tries)
|
|
{
|
|
if(step())
|
|
return true;
|
|
//--- A permanent refusal gives the same answer five times, and the retries push the one line
|
|
//--- that explains it off the top of the operator's log. Stop and repeat the reason instead.
|
|
if(g_initFatalReason != "")
|
|
{
|
|
Print(caller + ": Failed to " + what + " - " + g_initFatalReason +
|
|
". Retrying cannot change this; see the REFUSED line above.");
|
|
return false;
|
|
}
|
|
Print(caller + ": Failed to " + what + ", retrying...");
|
|
RandomSleep();
|
|
}
|
|
Print(caller + ": Failed to " + what + " after retries");
|
|
return false;
|
|
}
|
|
//--- The four steps above, each as the no-argument call RetryInitStep takes. Thin by necessity:
|
|
//--- MQL5 function pointers cannot bind a method call or an argument, and these are two of each.
|
|
//--- WarriorBookMagic(true), not the raw input: with Expert_MagicNumber at 0 the real magic is the
|
|
//--- assigned-and-remembered one, and m_magic must be the LONG book so every inherited CExpert path
|
|
//--- addresses the same book SelectPosition() defaults to.
|
|
bool StepExpertInit(void) { return Expert.Init(Symbol(), Period(), Expert_EveryTick, WarriorBookMagic(true)); }
|
|
bool StepInitTrailing(void) { return InitializeTrailing(); }
|
|
bool StepInitMoneyManagement(void) { return InitializeMoneyManagement(); }
|
|
bool StepValidateSettings(void) { return Expert.ValidationSettings(); }
|
|
bool StepInitIndicators(void) { return Expert.InitIndicators(); }
|
|
//--- 500ms: Train() does at most era.budgetMs of work per call, then yields back here.
|
|
//--- EventSetMillisecondTimer is needed for sub-second resolution; EventSetTimer takes whole
|
|
//--- seconds only.
|
|
#define WARRIOR_TIMER_INTERVAL_MS 500
|
|
//+------------------------------------------------------------------+
|
|
//| NO SUB-SECOND TIMER IN THE TESTER. The tester fires OnTimer on |
|
|
//| SIMULATED time, so a 500ms timer over a 2016-2026 pass is |
|
|
//| ~600 MILLION OnTimer calls - each walking 4x PollTraining, the |
|
|
//| vote readout's string build, the overlay advance and the deployed |
|
|
//| census - none of which an inference-only pass needs: training |
|
|
//| never runs (m_inferenceOnly), inference is driven per bar by |
|
|
//| OnTickHandler off the tick stream, the risk budget is re-checked |
|
|
//| in OnTick, and there is no chart, panel or overlay to keep fresh. |
|
|
//| Measured 2026-08-25: 12 agents, 78 minutes, ZERO of 39 passes |
|
|
//| finished ("0.1% an hour"); the timer flood was the largest single |
|
|
//| consumer left after the optcache/DB/deinit fixes. An hourly |
|
|
//| EventSetTimer stays armed as a belt: anything that genuinely |
|
|
//| needs an occasional timer still gets ~one call per simulated hour |
|
|
//| (~2,600 per pass) instead of six hundred million. |
|
|
//+------------------------------------------------------------------+
|
|
bool StepSetTimer(void)
|
|
{
|
|
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
|
|
return EventSetTimer(3600);
|
|
return EventSetMillisecondTimer(WARRIOR_TIMER_INTERVAL_MS);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnInit PHASES below, in the exact order OnInit calls them. Split |
|
|
//| out of one ~430-line function per the DRY/KISS/SOLID sweep - each|
|
|
//| is one boot concern, called once, in the order the comments in |
|
|
//| OnInit's own body require (alt-data/cross-asset before any model |
|
|
//| build, filters added exactly once before the DB retry loop, |
|
|
//| etc.) - see OnInit for the call sequence and why it is fixed. |
|
|
//+------------------------------------------------------------------+
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Delete ORPHANED CONTROL PANELS. CAppDialog names every one of its |
|
|
//| objects <numeric instance id><control name> - NOT with a Warrior |
|
|
//| prefix - so a force-killed OnDeinit (MetaTrader's ~4,500 ms |
|
|
//| budget) strands a complete 15-object panel that no prefix sweep |
|
|
//| can ever match, and a re-attach mints a NEW instance id, so the |
|
|
//| ghost is permanent. Observed 2026-08-25 18:23: two killed charts |
|
|
//| each kept a full "17984Back...17984ResetDB" set, and XTIUSD had |
|
|
//| carried a "29641*" set across sessions. |
|
|
//| |
|
|
//| SAFETY: a numeric prefix qualifies only when at least FOUR of OUR |
|
|
//| button names (CreateButtons, Panel\ControlPanel.mqh) carry it - a |
|
|
//| dead panel always carries all nine, while a foreign CAppDialog |
|
|
//| would have to coincide on four of them (Save/Load alone is |
|
|
//| plausible; Save+Load+Pause+Deploy is not) - so another EA's or an |
|
|
//| indicator's panel on the same chart is never deleted by name |
|
|
//| coincidence. |
|
|
//+------------------------------------------------------------------+
|
|
int PurgeOrphanedPanelObjects(void)
|
|
{
|
|
string buttons[9] = {"Report", "Signals", "Pause", "Stop", "Deploy", "Save", "Load", "Reset", "ResetDB"};
|
|
string chrome[6] = {"Back", "Border", "Caption", "ClientBack", "Close", "MinMax"};
|
|
//--- Pass 1: find the numeric prefixes that own our buttons. Bounded small: a chart carries at
|
|
//--- most a handful of dead panels, one per kill.
|
|
string prefixes[];
|
|
int hits[];
|
|
int nPrefixes = 0;
|
|
int total = ObjectsTotal(0, -1, -1);
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
string nm = ObjectName(0, i, -1, -1);
|
|
for(int b = 0; b < 9; b++)
|
|
{
|
|
int cut = StringLen(nm) - StringLen(buttons[b]);
|
|
//--- Exact-suffix test; cut > 0 also rejects a bare button name with no prefix at all.
|
|
if(cut <= 0 || StringSubstr(nm, cut) != buttons[b])
|
|
continue;
|
|
string pre = StringSubstr(nm, 0, cut);
|
|
bool numeric = true;
|
|
for(int k = 0; k < StringLen(pre) && numeric; k++)
|
|
{
|
|
ushort c = StringGetCharacter(pre, k);
|
|
if(c < '0' || c > '9')
|
|
numeric = false;
|
|
}
|
|
if(!numeric)
|
|
continue;
|
|
int slot = -1;
|
|
for(int p = 0; p < nPrefixes; p++)
|
|
if(prefixes[p] == pre)
|
|
{
|
|
slot = p;
|
|
break;
|
|
}
|
|
if(slot < 0)
|
|
{
|
|
ArrayResize(prefixes, nPrefixes + 1);
|
|
ArrayResize(hits, nPrefixes + 1);
|
|
prefixes[nPrefixes] = pre;
|
|
hits[nPrefixes] = 0;
|
|
slot = nPrefixes++;
|
|
}
|
|
hits[slot]++;
|
|
break; // one suffix match per object ("...Reset" cannot also end "...ResetDB")
|
|
}
|
|
}
|
|
//--- Pass 2: for every qualifying prefix, delete the full known object set by NAME. ObjectDelete
|
|
//--- on a missing name is a silent no-op, so a partially-created ghost costs nothing extra.
|
|
int removed = 0;
|
|
for(int p = 0; p < nPrefixes; p++)
|
|
{
|
|
if(hits[p] < 4)
|
|
continue;
|
|
for(int b = 0; b < 9; b++)
|
|
if(ObjectDelete(0, prefixes[p] + buttons[b]))
|
|
removed++;
|
|
for(int c = 0; c < 6; c++)
|
|
if(ObjectDelete(0, prefixes[p] + chrome[c]))
|
|
removed++;
|
|
PrintFormat("Warrior: removed a dead control panel (instance id %s) - CAppDialog objects carry a"
|
|
" numeric id, not a Warrior prefix, so only this by-name pass can reach one that"
|
|
" survived a force-killed deinit.", prefixes[p]);
|
|
}
|
|
return removed;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Sweep every EA-owned object namespace off the chart (chart |
|
|
//| objects outlive the process - see OnInit's own comment on why |
|
|
//| this runs first), then report what still doesn't match any of |
|
|
//| our prefixes so drift is visible, not just a bare count. |
|
|
//+------------------------------------------------------------------+
|
|
void PurgeStaleChartObjectsAndReport(const string caller)
|
|
{
|
|
int initLeftover = 0;
|
|
int initPurged = WarriorPurgeChartObjects(0, false, initLeftover);
|
|
//--- The one family the prefix sweep cannot reach - see PurgeOrphanedPanelObjects. Runs before
|
|
//--- the residue report below so a dead panel is removed rather than listed as "not ours".
|
|
initPurged += PurgeOrphanedPanelObjects();
|
|
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.", caller, initPurged,
|
|
(initLeftover > 0
|
|
? StringFormat(" (%d of them needed a by-name delete after the bulk call)", initLeftover)
|
|
: ""));
|
|
//--- AND SAY WHAT SURVIVED IT. Names, not just a count - a count cannot be acted on. Reporting is
|
|
//--- the whole intervention. "Removed N, zero by-name leftovers" only ever meant "nothing matching
|
|
//--- OUR PREFIXES remains" - it was never a statement about the chart, and on 2026-08-17 22:00 all
|
|
//--- three charts printed exactly that and still came up with duplicated panels.
|
|
{
|
|
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).", caller, unmatched, residue,
|
|
(unmatched > 12 ? StringFormat(" ... and %d more", unmatched - 12) : ""));
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Configure the account loss budget BEFORE anything can size or |
|
|
//| place a trade. Everything downstream (the money manager's clamp, |
|
|
//| the risk-guard veto) reads this one object, so it must be live |
|
|
//| from the first tick rather than from whenever the signal |
|
|
//| pipeline happens to initialise. |
|
|
//+------------------------------------------------------------------+
|
|
void ConfigureRiskBudget(void)
|
|
{
|
|
g_riskBudget.Configure(EnableRiskGuard, MaxDailyLossPct, MaxDrawdownPct, MaxDrawdownIsTrailing,
|
|
RiskDayResetHour, RiskPerTradeOfBudget, RiskGuardFlatten,
|
|
(long)WarriorBookMagic(true), 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);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Block on alt-data and cross-asset reference-pair warm-up BEFORE |
|
|
//| any model is built below: both InitNeuralNetwork's fingerprint/ |
|
|
//| input-width pinning (alt-data) and a fresh model's first Build() |
|
|
//| (cross-asset) happen once, at construction, and never re-widen |
|
|
//| or re-include data/pairs that land later - see AltDataReload's |
|
|
//| and WarmBlocking's declaration comments for the OLD failures |
|
|
//| this avoids. Skipped for an unmapped symbol / in the tester or |
|
|
//| optimizer, same guard OnTimer's upkeep tick uses. |
|
|
//+------------------------------------------------------------------+
|
|
void WarmExternalData(void)
|
|
{
|
|
if(EnableAltData && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION) &&
|
|
!MQLInfoInteger(MQL_FORWARD))
|
|
{
|
|
g_lastAltDataRun = TimeCurrent();
|
|
g_altDataFetch.Update(_Symbol);
|
|
}
|
|
if(EnableCrossAsset && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION) &&
|
|
!MQLInfoInteger(MQL_FORWARD))
|
|
{
|
|
CCrossAssetPanel crossAssetWarmer;
|
|
//--- A TIMEOUT HERE MUST BE AUDIBLE. Say so in the journal, because from that point on
|
|
//--- nothing else in the run ever mentions the missing pair again.
|
|
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.");
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| IS THE SIGNAL DATABASE LIVE THIS RUN? The one answer, because |
|
|
//| OnInit asks it twice (open+journal, then the transaction-cycle |
|
|
//| check) and a run where those two disagreed would try to open a |
|
|
//| database it never initialised. |
|
|
//| |
|
|
//| Off in the tester/optimizer. A backtest opens the DB under |
|
|
//| FILE_COMMON, which EVERY parallel optimization agent opens too - |
|
|
//| one SQLite file, N writers, and the per-tick journal Update() |
|
|
//| behind them. Measured 2026-08-25: 12 agents, zero passes finished |
|
|
//| in 75 minutes. |
|
|
//| |
|
|
//| It also buys nothing, for a reason specific to this EA's current |
|
|
//| shape: the DB's only effect on a trading decision is |
|
|
//| ApplyPatternWeight overriding a filter's module weight, and that |
|
|
//| is declined for any self-ranking filter (CExpertSignalCustom's |
|
|
//| !filter.SelfRanked() guard). The AI members self-rank once their |
|
|
//| tiers are measured, and the classic votes that DID consume the |
|
|
//| ranking are gone - so a tester run's DB is written and never |
|
|
//| read. If a future filter consumes DB ranking WITHOUT self-ranking,|
|
|
//| revisit this: a backtest would then stop reproducing live. |
|
|
//+------------------------------------------------------------------+
|
|
bool SignalDatabaseActive(void)
|
|
{
|
|
if(!UseDatabaseRanking)
|
|
return false;
|
|
return !(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION)
|
|
|| MQLInfoInteger(MQL_FORWARD));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Open (or create) this config's fingerprinted DB, wire the trade |
|
|
//| journal to it, and run the meta-corpus staleness check - or, |
|
|
//| with ranking off, put the journal in tracking-only mode so the |
|
|
//| expectancy stop still gets fed. Returns false to fail OnInit. |
|
|
//+------------------------------------------------------------------+
|
|
bool InitDatabaseAndJournal(const string caller)
|
|
{
|
|
//--- Deliberately NOT the whole journal: InitTrackingOnly below keeps close detection, MAE/MFE
|
|
//--- and the expectancy-stop feed alive, which a backtest genuinely has to simulate. Only the
|
|
//--- SQLite half is dropped - Update() already skips its INSERT when there is no DB.
|
|
if(SignalDatabaseActive())
|
|
{
|
|
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()). 4.0 (2026-08-19): DB timestamps switched from GMT to
|
|
//--- BROKER time (user decision: one clock everywhere).
|
|
const string dbVersion = "4.0";
|
|
PrintVerbose("Initializing Database...");
|
|
for(int tries = 0; !dbInitialized && tries < 5; ++tries)
|
|
{
|
|
if(!dbm.Init(dbVersion, databaseFolderStructure, dbName))
|
|
{
|
|
Print(caller + ": Failed to initialize Database, retrying...");
|
|
RandomSleep();
|
|
}
|
|
else
|
|
{
|
|
dbInitialized = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!dbInitialized)
|
|
{
|
|
Print(caller + ": Failed to initialize Database after retries");
|
|
return false;
|
|
}
|
|
//--- 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), WarriorBookMagic(true)))
|
|
{
|
|
Print(caller + ": Failed to initialize trade journal table");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
//--- Reached when ranking is off OR this is a tester/optimizer run (see the guard above).
|
|
//--- 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(WarriorBookMagic(true));
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Create every signal instance this run's Use_*/Enable* inputs ask |
|
|
//| for (AI architectures + the meta head + the classic votes + the |
|
|
//| filters), wire the meta gate onto the root signal, apply each |
|
|
//| classic vote's tuned periods, apply the shared AI configuration, |
|
|
//| and register every filter on the root signal EXACTLY ONCE (see |
|
|
//| the comment above the filter-registration block for why it must |
|
|
//| not live inside a retry loop). Returns false to fail OnInit. |
|
|
//+------------------------------------------------------------------+
|
|
bool CreateAndConfigureSignals(CExpertSignalCustom *signal, const int maxRetryOnError, const string caller)
|
|
{
|
|
//--- Re-derived from the per-NN inputs (they are also initialized at file scope in
|
|
//--- Variables\Variables.mqh; this re-assignment is the init-order-safe truth).
|
|
EnablePAI = Use_MLP;
|
|
EnableCONV = Use_CONV;
|
|
EnableLSTM = Use_LSTM;
|
|
EnableHYBRID = Use_CONVLSTM;
|
|
// 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);
|
|
//--- register whichever AI signal instances this run created, so the control panel can drive
|
|
//--- training/weight actions on exactly this run's current config (never another config's files)
|
|
g_aiSignalCount = 0;
|
|
if(EnablePAI && PAI != NULL)
|
|
RegisterAISignal(PAI);
|
|
if(EnableCONV && CONV != NULL)
|
|
RegisterAISignal(CONV);
|
|
if(EnableLSTM && LSTM != NULL)
|
|
RegisterAISignal(LSTM);
|
|
if(EnableHYBRID && HYBRID != NULL)
|
|
RegisterAISignal(HYBRID);
|
|
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((EnablePAI && PAI == NULL) || (EnableCONV && CONV == NULL) || (EnableLSTM && LSTM == NULL) || (EnableHYBRID && HYBRID == NULL) || (EnableNewsFilter && newsFilter == NULL) || (EnableSessionFilter && sessionFilter == NULL) || (EnableRiskGuard && riskGuard == NULL))
|
|
{
|
|
Print("Critical signal initialization failed, cannot proceed");
|
|
return false;
|
|
}
|
|
// 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).
|
|
if(EnableSessionFilter)
|
|
{
|
|
sessionFilter.TradeLondonSession(SF_trade_LondonSession);
|
|
sessionFilter.TradeNewYorkSession(SF_trade_NewYorkSession);
|
|
sessionFilter.TradeTokyoSession(SF_trade_TokyoSession);
|
|
}
|
|
if(EnableNewsFilter)
|
|
{
|
|
newsFilter.SetMinImpact(NF_MinImpact);
|
|
newsFilter.SetLookbackMinutes(NF_LookMinutes);
|
|
}
|
|
if(EnablePAI)
|
|
ConfigureAISignal(PAI);
|
|
if(EnableCONV)
|
|
ConfigureAISignal(CONV);
|
|
if(EnableLSTM)
|
|
ConfigureAISignal(LSTM);
|
|
if(EnableHYBRID)
|
|
ConfigureAISignal(HYBRID);
|
|
// Add filters
|
|
PrintVerbose("Initializing Signal filters...");
|
|
//--- added exactly once, before the DB retry loop below - these calls don't depend on DB success at
|
|
//--- all (every pointer here was already validated non-NULL above), but living inside the loop body
|
|
//--- meant a DB open/transaction failure that triggered a retry would re-run AddFilterToSignal() and
|
|
//--- register the same filter pointer a second time in signal's CArrayObj; since that array frees its
|
|
//--- elements on destruction, a duplicate entry means the same pointer gets deleted twice on shutdown
|
|
//--- (heap corruption), which could easily explain instability across a later remove/re-add cycle.
|
|
bool filtersAdded = true;
|
|
filtersAdded &= (EnableSessionFilter ? AddFilterToSignal(signal, sessionFilter) : true);
|
|
filtersAdded &= (EnableNewsFilter ? AddFilterToSignal(signal, newsFilter) : true);
|
|
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);
|
|
if(!filtersAdded)
|
|
{
|
|
Print(caller + ": Error loading filters");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Confirm the DB open/begin-transaction/commit/close cycle works, |
|
|
//| retrying up to maxRetryOnError times. Deliberately does NOT |
|
|
//| touch signal/filter state - CreateAndConfigureSignals() already |
|
|
//| registered every filter exactly once, so a retry here can never |
|
|
//| re-run that registration. Returns false to fail OnInit. |
|
|
//+------------------------------------------------------------------+
|
|
bool VerifyDatabaseTransactionCycle(const string caller, const int maxRetryOnError)
|
|
{
|
|
bool filterSuccess = false;
|
|
for(int tries = 0; tries < maxRetryOnError; ++tries)
|
|
{
|
|
//--- SignalDatabaseActive(), not UseDatabaseRanking: in the tester InitDatabaseAndJournal
|
|
//--- never called dbm.Init(), so opening here would fail and burn every retry.
|
|
if(SignalDatabaseActive() && !dbm.OpenDatabase())
|
|
{
|
|
Print(caller + ": Error opening database, retrying...");
|
|
RandomSleep();
|
|
continue;
|
|
}
|
|
if(SignalDatabaseActive() && !dbm.BeginTransaction())
|
|
{
|
|
Print(caller + ": Error starting transaction, retrying...");
|
|
dbm.CloseDatabase(); // Ensure the database is closed before retry
|
|
RandomSleep();
|
|
continue;
|
|
}
|
|
if(SignalDatabaseActive() && (!dbm.CommitTransaction() || !dbm.CloseDatabase()))
|
|
{
|
|
Print(caller + ": Error committing transaction or closing database, retrying...");
|
|
RandomSleep();
|
|
continue;
|
|
}
|
|
filterSuccess = true;
|
|
break; // Success if all operations complete without error
|
|
}
|
|
if(!filterSuccess)
|
|
{
|
|
Print(caller + ": Failed after all retries");
|
|
return false; // Return failure if retries are exhausted
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Create the control panel and do the one-time chart-object |
|
|
//| cleanup/event-flag setup that only matters once the panel (or |
|
|
//| its absence) is settled. |
|
|
//+------------------------------------------------------------------+
|
|
void FinalizeStartupUI(const string caller)
|
|
{
|
|
if(!CreateControlPanel())
|
|
Print(caller + ": WARNING - control panel failed to initialize; trading/training continue normally, "
|
|
"but no GUI panel will be available for this run");
|
|
//--- one-time cleanup: an earlier build used Comment() plus a separate background rectangle object
|
|
//--- that turned out to render ON TOP of the text (Comment() has no built-in background/styling
|
|
//--- parameters at all) - delete any leftover from a prior run now that status text is a single
|
|
//--- self-contained OBJ_LABEL (see SetStatusLabel()) with its own BGCOLOR fill instead
|
|
if(ObjectFind(0, "WarriorCommentBG") >= 0)
|
|
ObjectDelete(0, "WarriorCommentBG");
|
|
//--- required for CAppDialog's caption-bar drag to work at all - without it, the chart never delivers
|
|
//--- CHARTEVENT_MOUSE_MOVE and the panel silently ignores drag attempts
|
|
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| IMPORTANT: no failure branch below (nor in any helper it calls - |
|
|
//| AddFilterToSignal(), InitializeSignal(), InitializeTrailing(), |
|
|
//| InitializeMoneyManagement()) may call Expert.Deinit() before |
|
|
//| returning INIT_FAILED/false. |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
//--- FIRST: adopt the chart's tuned indicator periods (if a gated auto-tune install ever wrote them).
|
|
//--- Must precede ComputeDbConfigFingerprint() and the classic-signal configuration below, both of
|
|
//--- which consume the g_Tuned* values - see Variables\TunedPeriods.mqh for the whole contract.
|
|
LoadTunedPeriods();
|
|
//--- Arm the tester pass self-profile - see its globals above OnTick().
|
|
g_tpActive = (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD));
|
|
//--- clears out whatever status label text was left over from before this OnInit() ran (stale text
|
|
//--- from a prior "warm" re-init - e.g. an input-parameter change, which reuses this same running
|
|
//--- instance rather than a fresh one - would otherwise sit unchanged and look like nothing is
|
|
//--- happening) so it's obvious the moment training/signal init actually resumes producing new status text
|
|
//--- START FROM A GUARANTEED-CLEAN CHART. Before anything is drawn, sweep every object namespace this
|
|
//--- EA owns (WarriorChartPrefixes). Chart objects live in the chart PROFILE, not in the EA, so they
|
|
//--- outlive the process: a deinit that ran out of MetaTrader's ~4,500 ms budget, a crash, a terminal
|
|
//--- kill, or an .ex5 replaced while attached all leave objects behind that no later deinit will ever
|
|
//--- own. Deleting the EA's files does not remove them either, which is why they read as corruption.
|
|
//--- Arrows are INCLUDED in this sweep: LoadChartSignals restores them from their sidecar moments later
|
|
//--- and already opens with its own arrow sweep, so purging here costs nothing and removes any orphan
|
|
//--- that the sidecar does not account for - the ones that would otherwise be adopted by the next model
|
|
//--- to attach, because SaveChartSignals rebuilds that sidecar by SCANNING the chart.
|
|
//--- Runs BEFORE SetStatusLabel below, or it would delete the label it just created.
|
|
PurgeStaleChartObjectsAndReport(__FUNCTION__);
|
|
SetStatusLabel("Warrior EA: initializing...");
|
|
//--- WHICH BINARY IS ACTUALLY RUNNING. Read it FIRST when a fix appears not to have taken.
|
|
PrintFormat("%s: build tag %s | COMPILED %s", __FUNCTION__, WARRIOR_BUILD_TAG,
|
|
TimeToString(__DATETIME__, TIME_DATE | TIME_MINUTES));
|
|
PrintFormat("%s: trade settings snapshot - NNs=%s Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d TrailingStrategy=%d MM_STRATEGY=%d",
|
|
__FUNCTION__, EnabledNNSummary(), (int)Entry_Multiplier, (int)SL_Mode, (int)TP_Mode,
|
|
(int)TrailingStrategy, (int)MM_STRATEGY);
|
|
//--- HARD GATE on every trade-management enum. This is not hypothetical: MT5 replays a saved .set
|
|
//--- (or a stored optimization pass) without validating enum members, so an option removed between
|
|
//--- builds keeps being fed back in. Five such options were removed 2026-08-25 across four of these
|
|
//--- enums, and the stalest of them (SL_Mode = -1) would now place a stop on the wrong side of entry.
|
|
if(!ValidateTradeManagementInputs())
|
|
return INIT_FAILED;
|
|
if(!ValidateRiskInputs())
|
|
return INIT_FAILED;
|
|
ConfigureRiskBudget();
|
|
LoadSignalsVisibilityState();
|
|
int maxRetryOnError = 5;
|
|
string functionName = __FUNCTION__;
|
|
// Initialize random seed based on the number of milliseconds since the system started
|
|
//--- One process-wide seeding at startup; every fresh topology re-seeds with its own model id
|
|
//--- on top of this (System\Random.mqh).
|
|
WarriorRandSeed("OnInit");
|
|
// Initialize expert
|
|
if(!RetryInitStep(StepExpertInit, "initialize expert", maxRetryOnError, functionName))
|
|
return INIT_FAILED;
|
|
Expert.OnChartEventProcess(true);
|
|
//--- Alt-data and cross-asset warm-up MUST be on disk/synced BEFORE any model is built below - see
|
|
//--- WarmExternalData()'s declaration comment for why both are blocking, not left to the timer.
|
|
WarmExternalData();
|
|
// Creating signal
|
|
PrintVerbose("Initializing Signal...");
|
|
CExpertSignalCustom* signal = CreateSignalWithRetry<CExpertSignalCustom>(maxRetryOnError, true);
|
|
if(signal == NULL)
|
|
return INIT_FAILED;
|
|
InitializeSignal(signal);
|
|
// Initializing Database
|
|
if(!InitDatabaseAndJournal(functionName))
|
|
return INIT_FAILED;
|
|
//+------------------------------------------------------------------+
|
|
//| The per-NN inputs (Use_MLP/Use_CONV/Use_LSTM/Use_CONVLSTM) pick |
|
|
//| which direction architectures this run trades/trains - any |
|
|
//| subset, each an independent model. |
|
|
//+------------------------------------------------------------------+
|
|
if(!CreateAndConfigureSignals(signal, maxRetryOnError, functionName))
|
|
return INIT_FAILED;
|
|
//--- DB open/begin/commit/close cycle, retried - deliberately AFTER filter registration above (see
|
|
//--- CreateAndConfigureSignals()'s own comment on why filters must not sit inside this retry loop).
|
|
if(!VerifyDatabaseTransactionCycle(functionName, maxRetryOnError))
|
|
return INIT_FAILED;
|
|
// Trailing logic
|
|
PrintVerbose("Initializing Trailing...");
|
|
if(!RetryInitStep(StepInitTrailing, "initialize Trailing", maxRetryOnError, functionName))
|
|
return INIT_FAILED;
|
|
// Creation of money object
|
|
if(!RetryInitStep(StepInitMoneyManagement, "initialize Money Management", maxRetryOnError, functionName))
|
|
return INIT_FAILED;
|
|
// Check all trading objects parameters
|
|
PrintVerbose("Validating settings...");
|
|
if(!RetryInitStep(StepValidateSettings, "validate settings", maxRetryOnError, functionName))
|
|
return INIT_FAILED;
|
|
// Tuning of all necessary indicators
|
|
PrintVerbose("Initializing Indicators...");
|
|
if(!RetryInitStep(StepInitIndicators, "initialize Indicators", maxRetryOnError, functionName))
|
|
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.
|
|
//--- NEVER in the tester/optimizer/forward pass: TimeCurrent() there is SIMULATED, so the manual
|
|
//--- throttle at DB_RANKING_INTERVAL_SECONDS (below, paced off the same simulated clock) never
|
|
//--- actually elapses - this ran on essentially every one of the belt's ~2,600 fires per pass
|
|
//--- until this guard, opening the DB and running ProcessBufferedSignals()/UpdateSignalsWeights()
|
|
//--- every time. A tester pass is inference-only (m_inferenceOnly); DB-derived filter weights are
|
|
//--- a live-learning feature with nothing to update on a pass that never trains.
|
|
if(UseDatabaseRanking && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION) &&
|
|
!MQLInfoInteger(MQL_FORWARD))
|
|
Expert.OnTimerProcess(true);
|
|
//--- Interval and the reasoning behind it: WARRIOR_TIMER_INTERVAL_MS, beside StepSetTimer().
|
|
if(!RetryInitStep(StepSetTimer, "set the timer", maxRetryOnError, functionName))
|
|
return INIT_FAILED;
|
|
//--- THE COMBINED-VOTE ARROW LAYER. Bound here, at the end of init, because the key depends on the
|
|
//--- config fingerprint and the thresholds depend on the validated inputs - both settled by now.
|
|
//--- Keyed on symbol + period + config fingerprint rather than on any one model's filename: the vote
|
|
//--- belongs to the CHART, and on an ensemble no single member owns it.
|
|
//--- THE PINNED THRESHOLD IF THERE IS ONE, and the seed only before the first era has been scored.
|
|
//--- An arrow is a claim about a threshold, and the store DISCARDS its cache when that threshold
|
|
//--- moves - so handing it the Signal_ThresholdOpen seed while the chart trades a derived 15% both
|
|
//--- threw away every restorable arrow AND labelled the survivors with a threshold the EA does not
|
|
//--- use. LoadModelStats() has already restored g_ensDerivedThreshold by this point in init (its
|
|
//--- "restored the ensemble record" line prints before this one), which is what makes reading it here
|
|
//--- safe. Display-only: the trade path gets the same number from PublishVoteThreshold().
|
|
double arrowThreshold = (g_ensDerivedThreshold > 0.0) ? g_ensDerivedThreshold
|
|
: (double)Signal_ThresholdOpen;
|
|
g_voteArrows.Configure(WARRIOR_STATE_DIR + "ChartState\\" + StringFormat("WarriorVote_%s_%d_%s", _Symbol, (int)_Period, ComputeDbConfigFingerprint()),
|
|
arrowThreshold, (double)VOTE_EXIT_DISABLED_THRESHOLD,
|
|
//--- Inactive in the raw view (the per-model layer owns the chart there) and
|
|
//--- in the tester, whose charts are throwaway.
|
|
!DrawUnfilteredSignals && !(MQLInfoInteger(MQL_TESTER) ||
|
|
MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD)));
|
|
//--- A VOTE IS A CLAIM MADE BY A SPECIFIC SET OF MEMBERS. If any member rebuilt from scratch this
|
|
//--- run, every stored arrow was drawn by a model that no longer exists - restoring them shows a
|
|
//--- freshly-initialised ensemble's chart covered in a dead ensemble's calls, and the next
|
|
//--- snapshot writes them straight back out. Discard, do not Load.
|
|
if(g_warriorFreshTopologyThisRun)
|
|
g_voteArrows.Discard("a member rebuilt from scratch this run - these arrows belong to a model that no longer exists");
|
|
else
|
|
g_voteArrows.Load();
|
|
// Initialization successful
|
|
PrintVerbose("Initialization successful");
|
|
FinalizeStartupUI(functionName);
|
|
//--- WHICH POSITION MODEL IS ACTUALLY LIVE. Allow_Hedging is a request, not a guarantee: on a NETTING
|
|
//--- account the second book cannot exist, and silently running the single-position path while the
|
|
//--- input reads "true" is exactly the kind of gap that costs three wrong diagnoses later. Say it once.
|
|
if(Allow_Hedging && !WarriorHedgingActive())
|
|
PrintFormat("%s: Allow_Hedging is ON but this is a NETTING account (ACCOUNT_MARGIN_MODE=%d) -"
|
|
" running the single-position path. At most one position on %s, opposite votes"
|
|
" ignored. Nothing to fix in the EA; the account type decides this.",
|
|
functionName, (int)AccountInfoInteger(ACCOUNT_MARGIN_MODE), _Symbol);
|
|
else
|
|
if(WarriorHedgingActive())
|
|
PrintFormat("%s: TWO BOOKS live on %s - long book magic %I64u, short book magic %I64u, at most"
|
|
" one position each. An opposite vote OPENS the other book rather than closing"
|
|
" this one, so both positions keep the certification they were deployed under."
|
|
" The vote exit is pinned shut (threshold %d, unreachable).",
|
|
functionName, _Symbol, WarriorBookMagic(true), WarriorBookMagic(false),
|
|
VOTE_EXIT_DISABLED_THRESHOLD);
|
|
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;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| THE FOUR TESTER HANDLERS, AND WHERE EACH ONE ACTUALLY RUNS. |
|
|
//| |
|
|
//| OnTesterInit / OnTesterPass / OnTesterDeinit run in the CONTROLLING|
|
|
//| TERMINAL, once per optimization session - never on an agent, never |
|
|
//| once per pass. OnTester runs on the AGENT, at the end of each pass.|
|
|
//| Keeping them cheap matters for a different reason than OnDeinit |
|
|
//| does: the terminal blocks the whole optimization while they run. |
|
|
//+------------------------------------------------------------------+
|
|
int OnTesterInit()
|
|
{
|
|
IsBacktesting = true;
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
//--- Fires in the controlling terminal after each pass, but ONLY for passes that shipped frame data
|
|
//--- via FrameAdd(). This EA never calls FrameAdd, so this is unreachable today and is declared for
|
|
//--- one reason: without it, adding any frame-sending code later silently drops every frame instead
|
|
//--- of failing loudly. It deliberately does NOTHING but drain - reading frames here would put
|
|
//--- per-pass work on the terminal's critical path, which is exactly what stalls an optimization.
|
|
void OnTesterPass()
|
|
{
|
|
}
|
|
void OnTesterDeinit()
|
|
{
|
|
//--- Fires once at the END OF THE SESSION, not per pass - and by the time it runs, every pass's
|
|
//--- own OnDeinit() has already executed on its agent (including that pass's dbm.Deinit()).
|
|
//--- Nothing to tear down here: this process never opened a database, a chart or a model.
|
|
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);
|
|
bool isTesterRun = (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD));
|
|
//--- TESTER/OPTIMIZER FAST PATH. Everything between here and the shared teardown below exists to
|
|
//--- leave a CHART clean and a live model's state on disk. An optimization agent has neither: no
|
|
//--- visible chart to purge, no arrows worth persisting, and no weights to save (a tester run is
|
|
//--- inference-only by architecture - see m_inferenceOnly - so the weights are exactly what it
|
|
//--- loaded). Doing it anyway costs a per-signal arrow-sidecar WRITE plus two full chart-object
|
|
//--- scans on EVERY pass, which at optimization scale is hundreds of thousands of pointless file
|
|
//--- writes per agent and is the shape of thing that runs an agent into MetaTrader's ~4,500 ms
|
|
//--- deinit budget. Deliberately does NOT skip Expert.Deinit()/dbm.Deinit(): those free the signal
|
|
//--- tree and close any handle this pass opened, and leaking either across passes is how an agent
|
|
//--- accumulates its way into a stall.
|
|
if(isTesterRun)
|
|
{
|
|
//--- THE PASS'S OWN PROFILE - the answer to "where did this pass's hours go", printed by the
|
|
//--- pass itself. See the g_tp* globals above OnTick().
|
|
if(g_tpTicks > 0)
|
|
PrintFormat("tester pass profile: %I64d tick(s) - pre(risk/algo/autosave) %.1fs (%.1f us/tick),"
|
|
" Expert.OnTick %.1fs (%.1f us/tick), journal %.1fs (%.1f us/tick) | %I64d timer"
|
|
" event(s) - %.1fs total. The biggest bucket is where the next optimization"
|
|
" second goes.",
|
|
g_tpTicks,
|
|
g_tpPreUs / 1.0e6, (double)g_tpPreUs / g_tpTicks,
|
|
g_tpExpertUs / 1.0e6, (double)g_tpExpertUs / g_tpTicks,
|
|
g_tpJournalUs / 1.0e6, (double)g_tpJournalUs / g_tpTicks,
|
|
g_tpTimers, g_tpTimerUs / 1.0e6);
|
|
for(int i = 0; i < g_aiSignalCount; i++)
|
|
g_aiSignals[i].MarkShutdown();
|
|
//--- Discard any in-flight era rather than finalising it - the same call the live path makes,
|
|
//--- and the reason a killed pass never leaves a half-written era behind.
|
|
for(int i = 0; i < g_aiSignalCount; i++)
|
|
g_aiSignals[i].FlushTrainRun();
|
|
g_aiSignalCount = 0;
|
|
dbm.Deinit();
|
|
Expert.Deinit();
|
|
s_deinitInProgress = false;
|
|
return;
|
|
}
|
|
Print(__FUNCTION__ + ": shutting down - reason: " + reasonStr);
|
|
//--- NOTHING BEFORE THE VISIBLE CLEANUP MAY TOUCH THE DISK. Measured 2026-08-25 18:23 (terminal
|
|
//--- close, six charts): two charts printed the line above and then NOTHING for 5.9 s until
|
|
//--- "Abnormal termination" - killed inside the two file writes that used to sit here
|
|
//--- (SaveSignalsVisibilityState + the un-split vote-arrow save) while the four sibling charts
|
|
//--- flooded the same disk with their own saves. Every leftover object the operator saw traces to
|
|
//--- that: the purge never ran because a WRITE ahead of it blocked. So the order is now: capture
|
|
//--- what needs the chart (in memory), clean the chart, and only then open a single file.
|
|
ulong deinitT0 = GetMicrosecondCount();
|
|
ClearStatusLabel();
|
|
//--- THE VOTE ARROWS' SCAN HALF ONLY - before any purge removes the objects it reads from. This
|
|
//--- is the layer the chart shows with DrawUnfilteredSignals off, and it cannot be re-derived on
|
|
//--- the next attach without a full replay. The DISK half (WriteSnapshot) runs after the chart is
|
|
//--- clean, with the other persistence.
|
|
g_voteArrows.Snapshot();
|
|
//--- EARLY VISIBLE-UI SWEEP, 2026-08-16. Removes the vote arrows just captured above.
|
|
int earlySweepLeft = 0;
|
|
WarriorPurgeChartObjects(0, true, earlySweepLeft);
|
|
for(int i = 0; i < g_aiSignalCount; i++)
|
|
g_aiSignals[i].MarkShutdown();
|
|
//--- Destroy the control panel's own UI so CAppDialog removes its own objects cleanly (see
|
|
//--- Controls\Dialog.mqh). BEFORE the per-member sidecar saves (file writes) now: CAppDialog names
|
|
//--- its objects with a numeric instance id, not a Warrior prefix, so a panel that outlives a
|
|
//--- force-kill is the one ghost no prefix sweep can ever remove (see PurgeOrphanedPanelObjects) -
|
|
//--- it must go while the budget is still certain.
|
|
//--- No isTesterRun guard needed any more: a tester run returned at the fast path above.
|
|
ExtPanel.Destroy(reason);
|
|
if(g_altMapDialogOpen)
|
|
{
|
|
g_altMapDialog.Destroy(reason);
|
|
g_altMapDialogOpen = false;
|
|
}
|
|
ulong deinitTVisual = GetMicrosecondCount();
|
|
for(int i = 0; i < g_aiSignalCount; i++)
|
|
//--- Persist the drawn arrows to their sidecar and take them off the chart, on EVERY deinit
|
|
//--- reason. They come back on the next attach, from disk, if a model for that config exists -
|
|
//--- see PersistAndClearChartSignals(). The first file I/O of the teardown, and it runs with
|
|
//--- the label, panel and vote arrows already gone: with the raw view off (the shipped
|
|
//--- default) there are zero member arrows on the chart, so this is a scan plus an empty write.
|
|
g_aiSignals[i].ShutdownChartCleanup();
|
|
//--- FINAL SWEEP, after every owner-driven teardown has had its turn. Cheap and bounded: three
|
|
//--- prefix deletes plus one object-list scan, which is the shape of work this ordering rule
|
|
//--- permits at this point. It closes the ordinary case; OnInit closes the case where MetaTrader
|
|
//--- never let us finish.
|
|
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 deinitTArrows = GetMicrosecondCount();
|
|
Print(__FUNCTION__ + ": cleanup timings - visuals " + DoubleToString((deinitTVisual - deinitT0) / 1000.0, 0) +
|
|
" ms, member arrows " + DoubleToString((deinitTArrows - deinitTVisual) / 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 DISK, from here on - the chart is already clean whatever happens below. Small writes
|
|
//--- first, the weight save last.
|
|
SaveSignalsVisibilityState();
|
|
if(g_voteArrows.WriteSnapshot())
|
|
PrintVerbose(__FUNCTION__ + ": persisted " + IntegerToString(g_voteArrows.LastSaved()) + " combined-vote arrows");
|
|
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.
|
|
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.
|
|
//+------------------------------------------------------------------+
|
|
//| 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. |
|
|
//+------------------------------------------------------------------+
|
|
#define OVERLAY_BARS_PER_SLICE 150
|
|
//--- Minimum wall-clock between overlay re-arms. 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;
|
|
//--- Longest a sweep waits for a member that has stopped producing snapshots. Past this it draws
|
|
//--- with whoever is ready AND SAYS SO - a stale chart that never redraws is worse than a partial
|
|
//--- one that reports itself.
|
|
#define OVERLAY_PARTIAL_ARM_MS 600000
|
|
//+------------------------------------------------------------------+
|
|
//| Which ensemble members must have a snapshot before a sweep runs. |
|
|
//| |
|
|
//| The array index is the member's m_ensembleIndex, so the slot IS |
|
|
//| the bit. Zero means no ensemble is registered (a single-model |
|
|
//| configuration), and the gate is then a no-op rather than a lock. |
|
|
//+------------------------------------------------------------------+
|
|
uint EnrolledOverlayMask(void)
|
|
{
|
|
uint mask = 0;
|
|
int n = MathMin(ArraySize(g_warriorEnsemble), ENS_MAX_MEMBERS);
|
|
for(int i = 0; i < n; i++)
|
|
if(CheckPointer(g_warriorEnsemble[i]) != POINTER_INVALID)
|
|
mask |= (((uint)1) << i);
|
|
return mask;
|
|
}
|
|
void AdvanceFilteredSignalOverlay(void)
|
|
{
|
|
if(DrawUnfilteredSignals)
|
|
return; // raw view owns the chart; nothing to reconstruct
|
|
//--- RESTORE BEFORE RECONSTRUCT. The sidecar holds the arrows the previous session actually drew;
|
|
//--- the sweep below can only rebuild them while members still publish era-end snapshots, which a
|
|
//--- deployed ensemble no longer does. Draining the queue first means a reloaded chart shows its
|
|
//--- history immediately instead of waiting on a sweep that may never be armed.
|
|
if(g_voteArrows.Pending())
|
|
{
|
|
g_voteArrows.AdvanceRestore();
|
|
return; // one chart-drawing job at a time - both are budgeted per slice
|
|
}
|
|
//--- 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.
|
|
bool wantArm = g_warriorOverlayArmRequest || (g_aiSignalCount == 0 && g_lastOverlayArmTick == 0);
|
|
//--- EVERY ENROLLED MEMBER FIRST. A member still mid-era has no snapshot, the sweep skips it
|
|
//--- before the divisor, and the remaining member's own tier weight becomes the whole vote - a
|
|
//--- consensus arrow drawn from one model. See g_warriorOverlayReadyMask.
|
|
uint enrolled = EnrolledOverlayMask();
|
|
bool everyoneReady = (enrolled == 0) || ((g_warriorOverlayReadyMask & enrolled) == enrolled);
|
|
if(wantArm && !everyoneReady)
|
|
{
|
|
if(g_warriorOverlayArmSince == 0)
|
|
g_warriorOverlayArmSince = GetTickCount();
|
|
//--- Bounded, so a member that stopped cannot freeze the chart. Drawing partial is allowed;
|
|
//--- drawing partial SILENTLY is not.
|
|
if(GetTickCount() - g_warriorOverlayArmSince >= OVERLAY_PARTIAL_ARM_MS)
|
|
{
|
|
PrintFormat("Warrior: filtered overlay drawing with a PARTIAL ensemble after %d s - members"
|
|
" ready 0x%X of 0x%X enrolled. The missing members have produced no era-end"
|
|
" snapshot, so they abstain from every bar in this sweep and the vote is"
|
|
" diluted toward the members that did finish. Expect fewer arrows than the live"
|
|
" vote would cast, not different ones.",
|
|
OVERLAY_PARTIAL_ARM_MS / 1000, g_warriorOverlayReadyMask, enrolled);
|
|
everyoneReady = true;
|
|
}
|
|
}
|
|
//--- Re-arm only between sweeps (mid-flight restart would strand the tail undrawn), and at most
|
|
//--- once a minute once everyone is in.
|
|
if(wantArm && everyoneReady && !Expert.FilteredOverlayPending()
|
|
&& (g_lastOverlayArmTick == 0 || GetTickCount() - g_lastOverlayArmTick >= OVERLAY_REARM_MIN_MS))
|
|
{
|
|
g_warriorOverlayArmRequest = false;
|
|
g_warriorOverlayReadyMask = 0;
|
|
g_warriorOverlayArmSince = 0;
|
|
g_lastOverlayArmTick = GetTickCount();
|
|
Expert.ArmFilteredOverlay();
|
|
}
|
|
//--- MIRROR THE CHART THE MOMENT A SWEEP FINISHES, not only at shutdown. A sweep is the only thing
|
|
//--- that rewrites this layer wholesale, so its completion is exactly when the file is stale - and
|
|
//--- saving here means a terminal that is killed rather than closed still leaves a good record.
|
|
bool sweepWasPending = Expert.FilteredOverlayPending();
|
|
Expert.AdvanceFilteredOverlay(OVERLAY_BARS_PER_SLICE);
|
|
if(sweepWasPending && !Expert.FilteredOverlayPending())
|
|
{
|
|
g_voteArrows.Save();
|
|
//--- BACKFILL AN EMPTY COMBINED-VOTE RECORD from the sweep's own arithmetic. The record
|
|
//--- (g_ensCumOosCorrect/Total) normally accrues once per era at pass-3 completion - and a
|
|
//--- deployed ensemble runs no further eras, so a chart whose .stats predate the record (the
|
|
//--- WST7 migration) kept "Vote win rate: measuring..." forever even after the replay pass
|
|
//--- rebuilt every member's ladder (user report 2026-08-25). The sweep scores exactly the
|
|
//--- population the record describes: bars whose reconstructed vote cleared the open threshold
|
|
//--- under the direction policy, against the same inline swing-pivot labels the replay used.
|
|
//--- Guards: deployed only (a training-time sweep must not pre-empt the era scorer), and only
|
|
//--- into an EMPTY record (never on top of real era history - a restored record wins).
|
|
long overlayFired = 0, overlayWins = 0;
|
|
if(Expert.TakeOverlayVoteScore(overlayFired, overlayWins)
|
|
&& overlayFired > 0 && g_ensCumOosTotal <= 0 && WarriorChartModelsDeployed())
|
|
{
|
|
g_ensCumOosCorrect = overlayWins;
|
|
g_ensCumOosTotal = overlayFired;
|
|
PublishEnsembleAccuracyLine(-1.0, 0);
|
|
PrintFormat("Warrior: combined-vote record backfilled from the reconstructed overlay - %d"
|
|
" call(s) at or above the %.0f%% threshold, %d correct (%d%%). Partly IN-SAMPLE"
|
|
" (the window includes bars the members trained on); the next genuine era-end"
|
|
" scoring pass supersedes it.",
|
|
(int)overlayFired, g_ensembleVoteThreshold, (int)overlayWins,
|
|
(int)MathRound(overlayWins * 100.0 / overlayFired));
|
|
//--- Persist NOW, into every member's .stats (the loader adopts the most complete copy) -
|
|
//--- the whole class of bug this repairs is state that existed in memory and was never
|
|
//--- written down.
|
|
for(int mi = 0; mi < ArraySize(g_warriorEnsemble); mi++)
|
|
if(CheckPointer(g_warriorEnsemble[mi]) != POINTER_INVALID)
|
|
g_warriorEnsemble[mi].OnlineSaveModelStats();
|
|
}
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
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. 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;
|
|
ulong tpT0 = g_tpActive ? GetMicrosecondCount() : 0;
|
|
if(g_tpActive)
|
|
g_tpTimers++;
|
|
//--- ANY FILE WHOSE ATOMIC SWAP LOST A RACE. A rename fails while a peer chart holds the destination
|
|
//--- open, and with 134 MB pool files that read lasts seconds - far too long to wait out inside a
|
|
//--- quote. The content is already written; only the swap is outstanding. Here, on the clock, is
|
|
//--- where it costs nothing, and landing it now beats waiting for the next full publish to rewrite
|
|
//--- the whole file. No-op (one integer compare) whenever nothing is pending, which is nearly always.
|
|
AtomicPromotePending();
|
|
//--- 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.
|
|
//--- NOT in the tester: this runs a real forward pass per ensemble member (ProspectiveVote ->
|
|
//--- DisplayInference -> Net.feedForward) whose only product is a chart HUD string, and a
|
|
//--- tester chart is already treated as throwaway elsewhere (see the vote-arrow layer skip
|
|
//--- in OnInit above).
|
|
if(!MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
|
|
Expert.RefreshVoteReadout();
|
|
//--- 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.
|
|
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) && !MQLInfoInteger(MQL_FORWARD))
|
|
{
|
|
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)
|
|
{
|
|
if(g_tpActive)
|
|
g_tpTimerUs += GetMicrosecondCount() - tpT0;
|
|
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)
|
|
{
|
|
if(g_tpActive)
|
|
g_tpTimerUs += GetMicrosecondCount() - tpT0;
|
|
return;
|
|
}
|
|
g_lastDbRankingRun = now;
|
|
Expert.OnTimer();
|
|
if(g_tpActive)
|
|
g_tpTimerUs += GetMicrosecondCount() - tpT0;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
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.
|
|
//--- 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()
|
|
{
|
|
//--- STOP FIRST - same reasoning as OnTimer's guard. Deliberately AHEAD of the risk-budget update
|
|
//--- too: a program that is unloading places no orders, so there is nothing left for the budget to
|
|
//--- protect.
|
|
if(IsStopped())
|
|
return;
|
|
ulong tp0 = g_tpActive ? GetMicrosecondCount() : 0;
|
|
CheckAlgoTradingState();
|
|
//--- FIRST, and before Expert.OnTick() can open anything.
|
|
g_riskBudget.Update();
|
|
//--- THE DERIVED VOTE THRESHOLD, before anything can act on the old one. The era verdict publishes
|
|
//--- g_ensDerivedThreshold; this is where it reaches the m_threshold_open the trade decision reads.
|
|
//--- Here rather than inside the signal because ExpertSignalCustom.mqh does not see the ensemble
|
|
//--- globals (it is the PARENT of the AI filter that declares them), and here rather than at init
|
|
//--- because the value does not exist until an era has been scored.
|
|
if(g_ensDerivedThreshold > 0.0)
|
|
Expert.PublishVoteThreshold((int)MathRound(g_ensDerivedThreshold));
|
|
AutosaveWeightsIfDue();
|
|
ulong tp1 = g_tpActive ? GetMicrosecondCount() : 0;
|
|
Expert.OnTick();
|
|
ulong tp2 = g_tpActive ? GetMicrosecondCount() : 0;
|
|
//--- 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();
|
|
if(g_tpActive)
|
|
{
|
|
g_tpTicks++;
|
|
g_tpPreUs += tp1 - tp0;
|
|
g_tpExpertUs += tp2 - tp1;
|
|
g_tpJournalUs += GetMicrosecondCount() - tp2;
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
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.
|
|
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);
|
|
//--- 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);
|
|
//--- THE SEED ONLY. The real open threshold is derived per era and pushed in by
|
|
//--- CExpertCustom::PublishVoteThreshold(); this is what trades until the first era is scored.
|
|
signal_obj.ThresholdOpen((int)Signal_ThresholdOpen);
|
|
//--- THE EXIT, AS ONE BOOLEAN. OFF pins the close threshold to an arithmetically unreachable 101
|
|
//--- (the stock default of 100 IS reachable by a weighted mean of values capped at 100, which is
|
|
//--- why this is set explicitly rather than left alone). ON pins it to the SAME threshold the
|
|
//--- entry uses - the seed here, then the derived value from PublishVoteThreshold() once an era
|
|
//--- has been scored. Either way there is no second number to tune.
|
|
signal_obj.ThresholdClose(Exit_On_Reversal_Vote ? (int)Signal_ThresholdOpen
|
|
: VOTE_EXIT_DISABLED_THRESHOLD);
|
|
//--- The dormant half of the same policy, finally armed. m_holdToBarrier short-circuits
|
|
//--- CheckClosePosition() before it ever looks at a threshold, and until now NOTHING SET IT - the
|
|
//--- disabled threshold was carrying the whole policy by itself.
|
|
signal_obj.HoldToBarrier(!Exit_On_Reversal_Vote);
|
|
//--- The hold-to-barrier exit policy went with the fractal target (withdrawn - see Inputs.mqh).
|
|
//--- Barrier-target models keep the vote exits and always did: their label IS the vote's own
|
|
//--- horizon.
|
|
//--- HYBRID is now one fused signal, so no separate AI quorum is needed here.
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
// Initialize Trailing
|
|
bool InitializeTrailing()
|
|
{
|
|
switch(TrailingStrategy)
|
|
{
|
|
case TRAILING_STRATEGY_NONE:
|
|
// No trailing strategy selected
|
|
return true;
|
|
case TRAILING_STRATEGY_ATR_x1:
|
|
case TRAILING_STRATEGY_ATR_x2:
|
|
case TRAILING_STRATEGY_ATR_x3:
|
|
{
|
|
// ATR Trailing Strategy
|
|
double multiplier = 0;
|
|
switch(TrailingStrategy)
|
|
{
|
|
case TRAILING_STRATEGY_ATR_x1:
|
|
multiplier = 1;
|
|
break;
|
|
case TRAILING_STRATEGY_ATR_x2:
|
|
multiplier = 2;
|
|
break;
|
|
case TRAILING_STRATEGY_ATR_x3:
|
|
multiplier = 3;
|
|
break;
|
|
}
|
|
CTrailingATR *trailing = new CTrailingATR;
|
|
if(trailing == NULL)
|
|
{
|
|
Print(__FUNCTION__ + ": error creating trailing");
|
|
return false;
|
|
}
|
|
// Set ATR Multiplier
|
|
trailing.Multiplier(multiplier);
|
|
if(!Expert.InitTrailing(trailing))
|
|
{
|
|
Print(__FUNCTION__ + ": error initializing trailing");
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// Add more trailing strategies if needed
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool InitializeMoneyManagement()
|
|
{
|
|
string functionName = __FUNCTION__;
|
|
switch(MM_STRATEGY)
|
|
{
|
|
case FIXED_RISK:
|
|
{
|
|
CMoneyFixedRisk *money = CreateAndInitMoney<CMoneyFixedRisk>(functionName);
|
|
if(money == NULL)
|
|
return false;
|
|
money.Percent(Money_Risk_Percent);
|
|
break;
|
|
}
|
|
case FIXED_LOT:
|
|
{
|
|
CMoneyFixedLot *money = CreateAndInitMoney<CMoneyFixedLot>(functionName);
|
|
if(money == NULL)
|
|
return false;
|
|
money.Lots(Money_FixLot_Lots);
|
|
break;
|
|
}
|
|
}
|
|
// Add more money management strategies if needed
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|