//+------------------------------------------------------------------+ //| 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 \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 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 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 - 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(maxRetryOnError, EnablePAI); CSignalCONV *CONV = CreateSignalWithRetry(maxRetryOnError, EnableCONV); CSignalLSTM *LSTM = CreateSignalWithRetry(maxRetryOnError, EnableLSTM); CSignalHYBRID *HYBRID = CreateSignalWithRetry(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(maxRetryOnError, EnableNewsFilter); CSignalSessionFilter *sessionFilter = CreateSignalWithRetry(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(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(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(functionName); if(money == NULL) return false; money.Percent(Money_Risk_Percent); break; } case FIXED_LOT: { CMoneyFixedLot *money = CreateAndInitMoney(functionName); if(money == NULL) return false; money.Lots(Money_FixLot_Lots); break; } } // Add more money management strategies if needed return true; } //+------------------------------------------------------------------+