Warrior_EA/Warrior_EA.mq5

1697 lines
82 KiB
MQL5
Raw Permalink Normal View History

feat(ai): logit-adjusted loss, replacing oversampling and the post-hoc prior Menon et al. 2021 (ICLR), "Long-tail learning via logit adjustment": add tau*log(prior_c) to each class logit inside the training gradient. Softmax CE on adjusted logits is consistent for BALANCED error - the metric checkpoint selection already ranks on - so the loss and the deploy decision finally optimize the same thing. The engine already computed a true softmax + categorical-CE gradient and wrote it over the per-neuron sigmoid delta, so this is an offset added to three logits in the two places that gradient is built (backProp scalar path and backPropOCL). No backend, kernel or DLL change; the forward pass and every inference path are untouched, which is the point - the network learns to absorb the offset, so its raw argmax becomes the balanced-optimal decision with nothing applied at inference. Replaces rather than stacks. Minority replay is disabled while this is on, and the post-hoc inference prior is forced off. Stacking is not a theoretical worry: simulated on the measured 1118/1119/34298 distribution in the weak-signal regime, plain CE collapses to Neutral (33.4% balanced, Buy 0%), replay reaches 48.1%, logit adjustment 50.9% with better balance - and BOTH together score 45.4% with Neutral recall at 0%, worse than either alone. Buda et al. 2018 predicts exactly that. Motivation from the six-chart run: every topology took one direction to ~50% recall and abandoned the other, the direction chosen arbitrarily (the batch-norm control went Buy 1% / Sell 42%, the inverse of the other five). One era in 1,301 cleared the per-class recall floor. Fingerprinted conditionally, so the converged 60.7% models on disk keep their filenames and stay loadable as the fallback. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:05:14 -04:00
//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
//--- MQL5 Market rule IV: "Products must not contain calls to any DLL". Uncomment the next line before
//--- compiling the Market-submission .ex5 - it compiles the WarriorDML.dll/WarriorCPU.dll #import blocks
//--- and every call into them out of AI\NeuronDirectML.mqh entirely (see that file), leaving OpenCL then
//--- plain-MQL5 CPU (AI\NeuronCPU.mqh) as the only compute tiers - zero DLL calls in the resulting
//--- binary. Leave commented out for the private/prop-firm deployment build, which keeps the DirectML/
//--- CPU-DLL fallback tier for hosts without OpenCL.
//#define WARRIOR_MARKET_BUILD
//--- MARKET build only: embed every custom indicator this EA calls via CiCustom so one self-contained
//--- .ex5 ships to the Market. Paths embed straight from the terminal's standard MQL5\Indicators\ folder
//--- (leading backslash = MQL5 root) - the SAME place the indicators already live for the private build's
//--- runtime load, so you maintain ONE copy. The indicators must be compiled x64/non-AVX (CLI compile, or
//--- disable AVX in the compiler options) or the embed fails with error 414. IMPORTANT: the leading-
//--- backslash root only resolves when the compiler knows the MQL5 tree - build the Market .ex5 from
//--- INSIDE the terminal (MQL5\Experts\...) or via the MetaEditor CLI with an include path. Compiling
//--- from an external folder in the GUI makes the root fall back to the MQL5\Files\ sandbox and the embed
//--- can't find the files. The private build uses bare names (no resources), so it builds from anywhere.
//--- The "::" reference paths are built by WARRIOR_CI() in Variables\IndicatorResources.mqh - keep in sync.
#ifdef WARRIOR_MARKET_BUILD
#resource "\\Indicators\\ADCumulativeDelta.ex5"
#resource "\\Indicators\\ADShorteningOfThrust.ex5"
#resource "\\Indicators\\ADWyckoffEventStream.ex5"
#resource "\\Indicators\\ADWyckoffFailedStructure.ex5"
#resource "\\Indicators\\ADWyckoffSignificantBarInversion.ex5"
#resource "\\Indicators\\ADZigZag.ex5"
#resource "\\Indicators\\ADMovingAverage.ex5"
#endif
//--- Inputs FIRST so the EA's own grouped inputs lead the Inputs tab. Safe because Inputs.mqh depends
//--- only on Enumerations\InputEnums.mqh (which carries a guarded ENUM_OPTIMIZATION copy) - no AI
//--- header needed. The AI\Network.mqh optimizer/CPU inputs are declared in that header (parsed later)
//--- and so render at the END, under Inputs.mqh's "NN Optimizer / Performance" divider.
#include "Variables\Inputs.mqh"
//--- database classes
#include "Database\DatabaseManager.mqh"
#include "Database\TradeJournalManager.mqh"
//--- available custom classes
#include "Expert\ExpertCustom.mqh"
#include "System\PrintVerbose.mqh"
#include "System\StatusLabel.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"
//+------------------------------------------------------------------+
//| The CustomIndicators\*.mq5 files (ADCumulativeDelta, |
//| ADShorteningOfThrust, ADWyckoffEventStream, |
//| ADWyckoffFailedStructure, ADWyckoffSignificantBarInversion, |
//| ADZigZag, and the unified ADMovingAverage) are loaded via |
//| CiCustom/IND_CUSTOM (see ExpertSignalAIBase.mqh / SignalMA.mqh). |
//| Name resolution is build-conditional - see |
//| Variables\IndicatorResources.mqh (WARRIOR_CI): |
//| - MARKET build (WARRIOR_MARKET_BUILD): each indicator is EMBEDDED |
//| as a #resource (block above) from the standard MQL5\Indicators\ |
//| folder and called directly via its "::" resource path, so one |
//| self-contained .ex5 ships to the Market with no external files. |
//| A resource indicator is NOT extracted to disk. Same single copy |
//| the private build already loads at runtime - compile x64/non-AVX |
//| (CLI, or disable AVX) or the embed fails w/ err 414, and build |
//| the Market .ex5 from inside the terminal tree (or CLI /inc) so |
//| the leading-backslash MQL5 root resolves (an external GUI compile|
//| falls back to the MQL5\Files\ sandbox and can't find them). |
//| - Private build: referenced by bare name and loaded from |
//| <MQL5>\Indicators\ at call time (faster than unpacking a |
//| resource; dev terminals already have them deployed). Init*() |
//| fails CiCustom::Create() with a clear log line if a matching |
//| .ex5 isn't present. |
//+------------------------------------------------------------------+
//
CExpertCustom Expert;
CDatabaseManager dbm();
CTradeJournalManager journal;
//+------------------------------------------------------------------+
//| Pointers to whichever AI signal instances this run actually |
//| created (per AIType - MLP/CONV/LSTM/HYBRID), so the control |
//| panel can drive training/weight actions on exactly the signal(s) |
//| in play this run and never touch another config's files. |
//+------------------------------------------------------------------+
#define MAX_AI_SIGNALS 3
//--- Printed at OnInit so tester logs prove which binary is actually running.
#define WARRIOR_BUILD_TAG "hotreload-arrows-v1"
CExpertSignalAIBase *g_aiSignals[MAX_AI_SIGNALS];
int g_aiSignalCount = 0;
int ResolveDenseTopologyLayers(AI_CHOICE aiType)
{
switch(aiType)
{
case MLP_4L:
return 4;
case MLP_3L:
return 3;
case CONV_2L:
case LSTM_2L:
case HYBRID_2L:
return 2;
default:
return 3;
}
}
void RegisterAISignal(CExpertSignalAIBase *sig)
{
if(sig == NULL || g_aiSignalCount >= MAX_AI_SIGNALS)
return;
g_aiSignals[g_aiSignalCount++] = sig;
}
//+------------------------------------------------------------------+
//| Control panel: a CAppDialog-based GUI (see Panel\ControlPanel.mqh) |
//| with show/hide signals, start/pause/stop training, and save/load/ |
//| delete-reset weights buttons for the currently-active AI |
//| signal(s) only. The dialog's own caption bar provides the show/ |
//| hide (minimize) control - no separate toggle button needed. |
//+------------------------------------------------------------------+
//--- 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;
bool g_signalsVisible = true;
#define SIGNAL_VISIBILITY_STATE_SUFFIX ".sigvis"
string SignalsVisibilityStateFile(void)
{
return eaName + "_" + Symbol() + "_" + IntegerToString(Period()) + SIGNAL_VISIBILITY_STATE_SUFFIX;
}
bool LoadSignalsVisibilityState(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return false;
string stateFile = SignalsVisibilityStateFile();
if(!FileIsExist(stateFile, FILE_COMMON))
return false;
int handle = FileOpen(stateFile, FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(handle == INVALID_HANDLE)
return false;
int storedVisible = FileReadInteger(handle);
FileClose(handle);
g_signalsVisible = (storedVisible != 0);
return true;
}
bool SaveSignalsVisibilityState(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return true;
string stateFile = SignalsVisibilityStateFile();
int handle = FileOpen(stateFile, FILE_COMMON | FILE_BIN | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(handle == INVALID_HANDLE)
{
Print(__FUNCTION__ + ": failed to open " + stateFile + " for write, error " + IntegerToString(GetLastError()));
return false;
}
FileWriteInteger(handle, g_signalsVisible ? 1 : 0, INT_VALUE);
FileClose(handle);
return true;
}
//--- true while Show Signals has queued a rescan on one or more g_aiSignals and is waiting for all of
//--- them to finish (see ToggleSignalsVisibility/FinalizeSignalsRescanIfDone) - each signal's own rescan
//--- is now chunked across PollTraining's timer slices (CExpertSignalAIBase::AdvanceChartSignalRescan)
//--- instead of blocking the button click, so visibility can only be (re)applied and the "shown" Alert
//--- fired once every instance's RescanPending() has cleared.
bool g_signalsRescanPending = false;
//--- tracks the last known AlgoTrading permission state (terminal "Algo Trading" toggle AND this
//--- EA's own "Allow Algo Trading" property) so a change is logged exactly once, not spammed every tick
bool g_lastAlgoTradingAllowed = true;
//--- OnDeinit() is not guaranteed to run on a terminal crash/power loss/forced kill, so weights
//--- would otherwise only be as fresh as the last fully-completed training era. A periodic
//--- autosave closes that gap without depending on UseDatabaseRanking's timer.
//--- Fires on NEW BAR CLOSE, not a fixed wall-clock interval (a prior 300s timer saved up to ~12x more
//--- often than an H1 chart ever has new state to persist - nothing changes between bar closes, since
//--- both training/era-end saves and OnlineLearnStep's continual-learning updates are themselves bar-
//--- driven). Every save atomically renames the SAME shared FILE_COMMON model file a Strategy Tester
//--- backtest may be concurrently reading via FileCopy (see CopyFileWithRetry's declaration comment) -
//--- cutting write frequency to the real update cadence directly shrinks that collision window instead
//--- of just papering over it with more retries.
datetime g_lastAutosaveBarTime = 0;
//--- last observed AllTrainingDeployed() value, so OnTimer() can spot training deploying itself (plateau
//--- ladder / era cap) and resync the panel's button labels exactly once on the transition
bool g_lastDeployedState = false;
//--- summarizes state across all currently-active AI signals for button labels;
//--- "paused"/"stopped" only report true if EVERY active signal agrees, so a mixed state
//--- (e.g. AIType=All with one signal paused and one running) still shows an actionable label
bool AllTrainingPaused(void)
{
if(g_aiSignalCount == 0)
return false;
for(int i = 0; i < g_aiSignalCount; i++)
if(!g_aiSignals[i].IsTrainingPaused())
return false;
return true;
}
bool AllTrainingStopped(void)
{
if(g_aiSignalCount == 0)
return false;
for(int i = 0; i < g_aiSignalCount; i++)
if(!g_aiSignals[i].IsTrainingStopped())
return false;
return true;
}
//--- "deployed" = every active signal has finalised a model and is running live inference rather than
//--- training. This is the state that makes Pause/Stop meaningless (there is no run to pause or stop),
//--- so it drives BOTH the Deploy button's own label and the n/a labels on those two - see
//--- RefreshControlPanelLabels().
bool AllTrainingDeployed(void)
{
if(g_aiSignalCount == 0)
return false;
for(int i = 0; i < g_aiSignalCount; i++)
if(!g_aiSignals[i].TrainingComplete())
return false;
return true;
}
//--- true only while at least one signal is still trainable AND has never checkpointed an era that
//--- cleared the per-class recall floor - i.e. deploying right now would ship a model that ignores Buy
//--- or Sell. Same bar the plateau ladder's automatic deploy refuses to cross on its own.
bool AnyDeployWouldSkipRecallFloor(void)
{
for(int i = 0; i < g_aiSignalCount; i++)
if(!g_aiSignals[i].TrainingComplete() && !g_aiSignals[i].HasRecallPassingCheckpoint())
return true;
return false;
}
void ApplySignalsVisibility(void)
{
for(int i = ObjectsTotal(0, 0, OBJ_ARROW) - 1; i >= 0; i--)
{
string name = ObjectName(0, i, 0, OBJ_ARROW);
ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
}
ChartRedraw(0);
}
void ToggleSignalsVisibility(void)
{
g_signalsVisible = !g_signalsVisible;
//--- Hide->Show is also the operator's manual "these arrows look stale" refresh: rescan each deployed
//--- model against recent history BEFORE re-showing, so Show Signals reveals a fresh set instead of
//--- just re-exposing whatever old render the .arrows sidecar happened to hold (see
//--- CExpertSignalAIBase::StartChartSignalRescan/AdvanceChartSignalRescan). Skipped on Hide - nothing
//--- to refresh when hiding, so that path stays instant.
//--- Rescans are QUEUED here, not run to completion - each is a real per-bar inference pass over up to
//--- SIGNAL_RESCAN_LOOKBACK_BARS bars, chunked across PollTraining's timer slices so the click handler
//--- never blocks. Visibility is applied and the "shown" Alert fires later, once
//--- FinalizeSignalsRescanIfDone() sees every instance's RescanPending() clear (called from OnTimer).
if(g_signalsVisible)
{
bool anyQueued = false;
for(int i = 0; i < g_aiSignalCount; i++)
if(g_aiSignals[i].StartChartSignalRescan())
anyQueued = true;
g_signalsRescanPending = anyQueued;
if(anyQueued)
{
//--- Immediate feedback that the click registered - the real "Hide Signals" label only lands
//--- once FinalizeSignalsRescanIfDone() runs RefreshControlPanelLabels() below.
ExtPanel.SetSignalsText("Scanning...");
return; // ApplySignalsVisibility()/labels/Alert deferred to FinalizeSignalsRescanIfDone()
}
}
ApplySignalsVisibility();
SaveSignalsVisibilityState();
}
//--- Called every OnTimer tick while g_signalsRescanPending: applies visibility and fires the "shown"
//--- Alert only once every queued rescan (see ToggleSignalsVisibility) has finished, since the arrows
//--- being toggled visible don't exist yet until each instance's AdvanceChartSignalRescan completes.
void FinalizeSignalsRescanIfDone(void)
{
if(!g_signalsRescanPending)
return;
for(int i = 0; i < g_aiSignalCount; i++)
if(g_aiSignals[i].RescanPending())
return; // at least one instance still scanning - check again next tick
g_signalsRescanPending = false;
ApplySignalsVisibility();
RefreshControlPanelLabels();
Alert("Warrior EA: signal arrows shown");
}
//--- keeps every button's label in sync with live training/signal-visibility state; safe/cheap to
//--- call after every panel action
void RefreshControlPanelLabels(void)
{
ExtPanel.SetSignalsText(g_signalsVisible ? "Hide Signals" : "Show Signals");
bool noAI = (g_aiSignalCount == 0);
//--- The four training buttons describe ONE state machine, so their labels are derived together
//--- rather than independently - otherwise the panel offers actions that silently do nothing.
//--- Deployed is the state that matters most: with a finalised model there is no run left to pause or
//--- stop, and the only meaningful move is to put it back into training (which is what Deploy toggles
//--- to). See CExpertSignalAIBase::DeployNow/RetrainDeployed.
bool deployed = !noAI && AllTrainingDeployed();
ExtPanel.SetPauseText(noAI ? "Pause Training (n/a)"
: deployed ? "Pause Training (deployed)"
: (AllTrainingPaused() ? "Resume Training" : "Pause Training"));
ExtPanel.SetStopText(noAI ? "Stop Training (n/a)"
: deployed ? "Stop Training (deployed)"
: (AllTrainingStopped() ? "Start Training" : "Stop Training"));
ExtPanel.SetDeployText(noAI ? "Deploy Model (n/a)" : (deployed ? "Retrain Model" : "Deploy Model"));
ChartRedraw(0);
}
//--- CAppDialog is user-draggable; a drag near an edge followed by shrinking the chart (or dragging
//--- past the visible area) can leave it partially or fully off-screen with no way to grab it back.
//--- Clamps it back inside the current chart bounds whenever the chart is resized/scrolled.
void ClampControlPanelToChart(void)
{
long chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
long chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
if(chartWidth <= 0 || chartHeight <= 0)
return;
int x = ExtPanel.Left();
int y = ExtPanel.Top();
int w = ExtPanel.Width();
int h = ExtPanel.Height();
int maxX = (int)chartWidth - w;
int maxY = (int)chartHeight - h;
int clampedX = (maxX < 0) ? 0 : MathMin(MathMax(x, 0), maxX);
int clampedY = (maxY < 0) ? 0 : MathMin(MathMax(y, 0), maxY);
if(clampedX != x || clampedY != y)
ExtPanel.Move(clampedX, clampedY);
}
//--- creates the control panel dialog once, from OnInit() - the standard CAppDialog usage pattern
//--- (create in OnInit, destroy in OnDeinit; see Controls\Dialog.mqh). CAppDialog::Destroy(REASON_PROGRAM)
//--- calls ExpertRemove() once the dialog has a valid PROGRAM_EXPERT type (i.e. on any call after the
//--- first successful Create()), so it must never be called speculatively/defensively before Create() -
//--- doing so would silently detach this EA from the chart the next time this function ran.
bool CreateControlPanel(void)
{
ResetLastError();
long chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int panelX1 = (chartWidth > CP_PANEL_W + CP_RIGHT_MARGIN + 20) ? (int)(chartWidth - CP_PANEL_W - CP_RIGHT_MARGIN) : 10;
if(!ExtPanel.Create(0, "WarriorCP", 0, panelX1, CP_Y0, panelX1 + CP_PANEL_W, CP_Y0 + CP_PANEL_H))
{
Print(__FUNCTION__ + ": failed to create control panel, error " + IntegerToString(GetLastError()));
return false;
}
if(!ExtPanel.Run())
{
Print(__FUNCTION__ + ": failed to run control panel, error " + IntegerToString(GetLastError()));
return false;
}
ExtPanel.ForceMaximize();
ClampControlPanelToChart();
//--- seed the transition watcher (see OnTimer) so a model that is ALREADY deployed at attach time
//--- doesn't register as a fresh transition on the first timer tick
g_lastDeployedState = AllTrainingDeployed();
RefreshControlPanelLabels();
return true;
}
//--- Blocking on purpose, unlike the Alert() calls below: this only ever runs in direct response to the
//--- trader clicking a destructive (red) panel button, so pausing for their yes/no is expected UX, not
//--- an unwanted stall of live trade management - MessageBox() briefly blocks the chart's UI thread,
//--- which is exactly what a "are you sure?" gate needs.
bool ConfirmDestructiveAction(string message)
{
return MessageBox(message + "\n\nThis action cannot be undone.", "Warrior EA - Confirm",
MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2) == IDYES;
}
//--- performs whatever button action ExtPanel recorded (see ConsumeAction() in ControlPanel.mqh);
//--- a no-op when nothing was clicked since the last call
void HandleControlPanelAction(ENUM_CP_ACTION action)
{
switch(action)
{
case CP_ACTION_TOGGLE_SIGNALS:
ToggleSignalsVisibility();
//--- If a rescan got queued (Show Signals with a deployed model to re-infer from), the label
//--- refresh + Alert are deferred to FinalizeSignalsRescanIfDone() - firing "shown" here would lie
//--- about arrows that don't exist on the chart yet. Hide, or a Show with nothing to rescan,
//--- completes synchronously so it's safe to report right away.
if(!g_signalsRescanPending)
{
RefreshControlPanelLabels();
Alert("Warrior EA: signal arrows " + (g_signalsVisible ? "shown" : "hidden"));
}
break;
case CP_ACTION_TOGGLE_PAUSE:
{
//--- A deployed model has no training run to pause - say so instead of silently doing nothing.
if(AllTrainingDeployed())
{
Alert("Warrior EA: the model is deployed - there is no training run to pause.\nUse \"Retrain Model\" first if you want to train it further.");
break;
}
bool pause = !AllTrainingPaused();
for(int i = 0; i < g_aiSignalCount; i++)
if(pause)
g_aiSignals[i].PauseTraining();
else
g_aiSignals[i].ResumeTraining();
RefreshControlPanelLabels();
Alert("Warrior EA: training " + (pause ? "paused" : "resumed"));
break;
}
case CP_ACTION_TOGGLE_STOP:
{
//--- Same as Pause: Stop/Start operate on a training run, and a deployed model isn't one.
//--- Routing this to RetrainDeployed() instead would silently do the Deploy button's job.
if(AllTrainingDeployed())
{
Alert("Warrior EA: the model is deployed and already running live inference, not training.\nUse \"Retrain Model\" to put it back into training.");
break;
}
bool doStop = !AllTrainingStopped();
for(int i = 0; i < g_aiSignalCount; i++)
if(doStop)
g_aiSignals[i].StopTraining();
else
g_aiSignals[i].StartTraining();
RefreshControlPanelLabels();
Alert("Warrior EA: training " + (doStop ? "stopped" : "restarted"));
break;
}
case CP_ACTION_TOGGLE_DEPLOY:
{
if(g_aiSignalCount == 0)
{
Alert("Warrior EA: no AI signal is active - nothing to deploy (set the AI algorithm input to something other than Disabled).");
break;
}
//--- RETRAIN direction: put the finalised model back into training, continuing from its own
//--- weights. Reversible, non-destructive (the weights on disk stay), so no confirmation.
if(AllTrainingDeployed())
{
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].RetrainDeployed();
RefreshControlPanelLabels();
Alert("Warrior EA: retraining the deployed model - it continues from its current weights.\nUse \"Delete & Reset Weights\" instead to start from scratch.");
break;
}
//--- DEPLOY direction. The plateau ladder refuses to auto-deploy a model that never cleared the
//--- per-class recall floor (it would be one that ignores Buy or Sell); a manual deploy is the
//--- operator's call, but they should make it knowingly - so this is the one case that asks.
if(AnyDeployWouldSkipRecallFloor() &&
!ConfirmDestructiveAction("No training era has cleared the per-class recall floor yet, so this model may "
"be ignoring Buy or Sell entirely.\n\nDeploy it anyway as the final model?"))
{
Alert("Warrior EA: deploy cancelled - training continues");
break;
}
int deployed = 0;
for(int i = 0; i < g_aiSignalCount; i++)
if(g_aiSignals[i].DeployNow())
deployed++;
RefreshControlPanelLabels();
if(deployed == 0)
Alert("Warrior EA: could not deploy - the AI signal is not initialised yet.");
else
Alert("Warrior EA: model deployed (" + IntegerToString(deployed) + " signal(s)).\nTraining stopped; it now runs live inference. Click \"Retrain Model\" to train it further.");
break;
}
case CP_ACTION_SAVE:
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].SaveWeightsNow();
Alert("Warrior EA: weights saved");
break;
case CP_ACTION_LOAD:
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].LoadWeightsNow();
//--- A reload carries the saved file's own "deployed" flag, so it can flip the whole training
//--- state machine (loading a finalised model makes Deploy read "Retrain Model", and Pause/Stop
//--- read "(deployed)"). Resync the labels or the panel would keep offering stale actions.
RefreshControlPanelLabels();
Alert("Warrior EA: weights reloaded from disk");
break;
case CP_ACTION_RESET:
if(!ConfirmDestructiveAction("Delete the saved AI weights and restart training from era 0?"))
{
Alert("Warrior EA: weights reset cancelled");
break;
}
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].ResetWeights();
RefreshControlPanelLabels();
Alert("Warrior EA: weights reset - training restarts from era 0");
break;
case CP_ACTION_REPORT:
{
if(!UseDatabaseRanking)
{
Print(__FUNCTION__ + ": trade journal report requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
Alert("Warrior EA: trade journal report requires \"Weight filters by DB win-rate\" to be enabled");
break;
}
string reportPath, reportError;
if(journal.GenerateReport(reportPath, reportError))
{
Print(__FUNCTION__ + ": trade journal report ready - " + reportPath);
Alert("Warrior EA: trade journal report exported - see the Experts log for the file path");
}
else
{
Print(__FUNCTION__ + ": could not generate trade journal report - " + reportError);
Alert("Warrior EA: could not export trade journal report - " + reportError);
}
break;
}
//--- separate from CP_ACTION_RESET on purpose: resetting AI weights (a routine, frequent action
//--- while tuning) must never cost the trader their accumulated pattern-confidence/trade-journal
//--- history, and vice versa - these are two independent "start fresh" decisions now.
case CP_ACTION_RESET_DB:
if(UseDatabaseRanking)
{
if(!ConfirmDestructiveAction("Delete the trade-journal and pattern-confidence database?"))
{
Alert("Warrior EA: database reset cancelled");
break;
}
dbm.ResetDatabase();
Alert("Warrior EA: database reset");
}
else
{
Print(__FUNCTION__ + ": database reset requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
Alert("Warrior EA: database reset requires \"Weight filters by DB win-rate\" to be enabled");
}
break;
default:
break;
}
}
// Helper function to pause execution for a random duration between 1 to 3 seconds
void RandomSleep()
{
Sleep(MathRand() % 2000 + 1000); // Sleeps between 1000ms (1s) and 3000ms (3s)
}
//+------------------------------------------------------------------+
//| Config fingerprint appended to the pattern-confidence/trade- |
//| journal database's filename, so a topology or feature-set change |
//| that would produce a differently-shaped/behaving model gets its |
//| own database instead of silently reusing pattern-weight/journal |
//| history that no longer matches what's actually trading now. Same |
//| FNV-1a scheme CExpertSignalAIBase uses for its own .nnw cache-key |
//| fingerprint (Expert\ExpertSignalAIBase.mqh), computed here from |
//| the raw inputs directly since the DB opens before any AI signal |
//| object exists to ask. |
//+------------------------------------------------------------------+
string ComputeDbConfigFingerprint()
{
refactor(ai): derive the first dense layer's width instead of asking for it InitialNeurons was an input whose only defensible value depends on two things the user cannot see when picking from a dropdown: how wide the input vector ended up after feature selection, and how much in-sample data the study period actually yields. Left to a hand-picked constant it was badly wrong - 500 units against a 420-wide input is 210,500 weights, 72% of a 292,583-weight model, against ~36,500 training bars of which only ~2,236 are directional. That is 6.6 weights per training bar, and it EXPANDS a set of highly correlated inputs rather than compressing them. The symptom was already in the logs and had been read as a depth problem: the shallowest topology consistently beat the deepest (perceptron 52.7% balanced, hybrid 41.3%). Over-parameterization predicts that ordering just as well as covariate shift does, and only one of the two had been addressed. ComputeFirstLayerWidth() budgets roughly one first-layer weight per in-sample bar. Measured across the configurations in use: M15 10y -> 256 units, 129,071 weights, 0.73 per bar H1 10y -> 64 units, 28,727 weights, 0.65 per bar H4 10y -> 16 units, 7,559 weights, 0.68 per bar Two design points that matter: - It estimates in-sample bars from the STUDY PERIOD and timeframe, not from Bars(). What is downloaded grows over a terminal's lifetime, and a topology that widened as history filled in would re-key its own weights file and discard a trained model. - The result is snapped down to a coarse power-of-two ladder, so the estimate would have to be wrong by ~2x to change the answer. Every field it reads is already part of the weights-filename fingerprint, so the derived value needs no fingerprint entry of its own. The public setter is removed - it could only have been called after construction, and would either be ignored or silently re-key the model mid-run. Where the data cannot support even the floor (D1 over 10 years is under 2,000 bars) it now says so and names the fixes, rather than quietly training a model with more weights than examples. The DB config fingerprint drops the term too, which re-keys existing pattern databases once - correct, since a model an order of magnitude smaller should not inherit the old one's win-rate history. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 13:01:16 -04:00
//--- InitialNeurons is gone from this key: the first-layer width is now DERIVED from the input width
//--- and the study period (CExpertSignalAIBase::ComputeFirstLayerWidth), not chosen, and every
//--- determinant of it that IS a user choice is already hashed here. Dropping the term re-keys
//--- existing databases once - which is correct, because a model an order of magnitude smaller
//--- genuinely is a different config and should not inherit the old one's pattern win-rate history.
refactor(ai): derive the dense taper's shape, not just its first layer Deriving the first layer's width left NeuronsReduction and MinNeuronsCount behind as inputs calibrated for something that no longer exists. Against a hand-picked 500-wide first layer "keep 30%, floor at 20" produced a genuine funnel - 500 -> 150 -> 45. Against the derived 64 it degenerates to 64 -> 20 -> 20: the reduction factor stops mattering after one step, and "minimum neurons per layer" silently becomes the width of every layer but the first. Two knobs whose labels no longer describe what they do. The taper now runs geometrically from the derived first-layer width down to a final hidden layer sized off the output count, spread evenly over however many layers the chosen AIType implies: MLP_3L 64 -> 28 -> 12 -> 3 29,151 dense weights MLP_4L 64 -> 37 -> 21 -> 12 -> 3 30,450 CONV/LSTM/HYBRID_2L 64 -> 12 -> 3 27,763 and it stays a funnel at the floor, where the old rule could not: D1 (first layer floored to 16) 16 -> 14 -> 12 -> 3 Both inputs are removed. With the width derived there is no freedom left in the taper, so keeping either would only let the user contradict the derivation. The layer COUNT stays selectable, because it is bundled into AIType alongside the conv/LSTM front-end - depth is an architecture choice, not a data-derived quantity, and pairing them means the two cannot contradict each other. m_minNeuronsCount / m_neuronsReduction survive as frozen members: nothing reads them to build a topology any more, but they hold positional slots in the .cfg sidecar and the weights fingerprint, and changing either value would re-key every model on disk for no behavioural reason. The DB config fingerprint drops both terms. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:03:42 -04:00
string fp = StringFormat("%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d",
(int)AIType, (int)OutputNeuronsCount, (int)TrainingOptimizer,
(int)ind_Periods, (int)StudyPeriods,
EnableVolume, EnableTime, EnableATR,
EnableMA, (int)PeriodMA, (int)MA_Type, EnableRSI, (int)PeriodRSI,
EnableSwingContext, EnableNews,
EnableADCumulativeDelta, EnableADShorteningOfThrust, EnableADWyckoffEventStream)
+ StringFormat("|%d|%d|%d|%d|%d|%d", EnableADWyckoffFailedStructure, EnableADWyckoffSignificantBarInversion, (int)LstmHiddenSize, (int)ConvFilterCount,
EnableMAFeature, EnableRSIFeature);
//--- MACD/Ichimoku, appended ONLY WHEN ENABLED - same rule, and same reason, as the matching block in
//--- CExpertSignalAIBase::BuildConfigFingerprint(): appending unconditionally would re-key every existing
//--- database the moment this shipped, orphaning the accumulated per-pattern win-rate history of configs
//--- that use neither. A run that enables either one genuinely IS a different config (new voting signals
//--- mean new pattern rows; new input features mean a differently-shaped model) and gets its own DB.
if(EnableMACD || EnableMACDFeature)
fp += StringFormat("|MACD:%d:%d:%d:%d:%d", EnableMACD, EnableMACDFeature,
(int)MACD_PeriodFast, (int)MACD_PeriodSlow, (int)MACD_PeriodSignal);
if(EnableIchimoku || EnableIchimokuFeature)
fp += StringFormat("|ICHI:%d:%d:%d:%d:%d", EnableIchimoku, EnableIchimokuFeature,
(int)Ichimoku_PeriodTenkan, (int)Ichimoku_PeriodKijun, (int)Ichimoku_PeriodSenkou);
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 real Depth of Market data is actually available for |
//| `symbol` before anything else (including neural network init) |
//| depends on it. MarketBookAdd() succeeding only means the |
//| SUBSCRIPTION was accepted - some brokers/symbols accept the |
//| subscription but never actually stream book data, so this also |
//| polls MarketBookGet() briefly for a genuinely non-empty snapshot |
//| before trusting it. Alerts the user (popup) and cleans up its own |
//| subscription on any failure so the caller can simply treat |
//| EnableMarketDepth as false for the rest of this run. |
//+------------------------------------------------------------------+
bool CheckMarketDepthAvailability(string symbol)
{
if(!MarketBookAdd(symbol))
{
Print("Warrior EA: Market Depth (DOM) is not available for " + symbol + " - the order-book confirmation filter will be omitted for this run. Your broker/account may not provide Depth of Market for this symbol.");
Print("Warrior EA: MarketBookAdd(" + symbol + ") failed, error " + IntegerToString(GetLastError()) + " - EnableMarketDepth will be treated as false.");
return false;
}
MqlBookInfo book[];
bool populated = false;
for(int attempt = 0; attempt < 10 && !populated; attempt++)
{
if(MarketBookGet(symbol, book) && ArraySize(book) > 0)
populated = true;
else
Sleep(200);
}
if(!populated)
{
MarketBookRelease(symbol);
Print("Warrior EA: Market Depth (DOM) subscribed for " + symbol + " but returned no data - your broker likely does not provide real Depth of Market for this symbol. The order-book confirmation filter will be omitted for this run.");
Print("Warrior EA: MarketBookGet(" + symbol + ") returned no levels after 2s - EnableMarketDepth will be treated as false.");
return false;
}
Print("Warrior EA: Market Depth (DOM) available for " + symbol + " - order-book confirmation filter enabled.");
return true;
}
// Helper function to retry signal creation with error handling
template <typename TSignal>
TSignal* CreateSignalWithRetry(int maxRetries, bool enableFlag)
{
if(!enableFlag)
return NULL;
TSignal* signal = NULL;
for(int tries = 0; tries < maxRetries; ++tries)
{
signal = new TSignal;
if(signal == NULL)
{
Print("Initialization of signal failed, retrying...");
RandomSleep();
}
else
break;
}
if(signal == NULL)
{
Print("Failed to create and initialize signal after retries");
}
return signal;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| IMPORTANT: no failure branch below (nor in any helper it calls - |
//| AddFilterToSignal(), InitializeSignal(), InitializeTrailing(), |
//| InitializeMoneyManagement()) may call Expert.Deinit() before |
//| returning INIT_FAILED/false. MQL5 ALWAYS calls this EA's own |
//| OnDeinit(REASON_INITFAILED) automatically once OnInit() returns |
//| anything other than INIT_SUCCEEDED, and OnDeinit() already calls |
//| Expert.Deinit() itself. Expert.Deinit() tears down `signal` and, |
//| through it, every registered AI signal (MLP/CONV/LSTM/HYBRID - see|
//| g_aiSignals' declaration comment) - calling it a second time here |
//| would free those objects while g_aiSignals[] still points at them, |
//| and OnDeinit()'s own PersistOnShutdown() loop over g_aiSignals[] |
//| would then dereference already-freed pointers. This is exactly |
//| what "invalid pointer access" during OnDeinit() after a failed |
//| OnInit() means if it ever recurs - the fix is to remove whichever |
//| inline Expert.Deinit() call was re-added, not to guard the loop. |
//+------------------------------------------------------------------+
int OnInit()
{
//--- 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
SetStatusLabel("Warrior EA: initializing...");
Print(__FUNCTION__ + ": build tag " + WARRIOR_BUILD_TAG);
PrintFormat("%s: trade settings snapshot - AIType=%d Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d Min_Risk_Reward_Ratio=%g TrailingStrategy=%d MM_STRATEGY=%d",
__FUNCTION__, (int)AIType, (int)Entry_Multiplier, (int)SL_Mode, (int)TP_Mode,
(double)Min_Risk_Reward_Ratio, (int)TrailingStrategy, (int)MM_STRATEGY);
LoadSignalsVisibilityState();
int maxRetryOnError = 5;
string functionName = __FUNCTION__;
// Initialize random seed based on the number of milliseconds since the system started
MathSrand(GetTickCount());
// Initialize expert
bool expertInitialized = false;
for(int tries = 0; !expertInitialized && tries < 5; ++tries)
{
if(!Expert.Init(Symbol(), Period(), Expert_EveryTick, Expert_MagicNumber))
{
Print(functionName + ": Failed initializing expert, retrying...");
RandomSleep();
}
else
{
expertInitialized = true;
break;
}
}
if(!expertInitialized)
{
Print(functionName + ": Failed to initialize expert after retries");
return INIT_FAILED;
}
Expert.OnChartEventProcess(true);
//--- Market Depth availability check - deliberately BEFORE any signal (and therefore any neural
//--- network) is created below, per the same "before initializing neurons" requirement as the
//--- AI models themselves. g_marketDepthAvailable stays false (its declaration default) if
//--- EnableMarketDepth is off, so nothing below needs to special-case that.
if(EnableMarketDepth)
g_marketDepthAvailable = CheckMarketDepthAvailability(_Symbol);
// Creating signal
PrintVerbose("Initializing Signal...");
CExpertSignalCustom* signal = NULL;
for(int tries = 0; signal == NULL && tries < 5; ++tries)
{
signal = new CExpertSignalCustom;
if(signal == NULL)
{
Print(functionName + ": Failed to initialize Signal, retrying...");
RandomSleep();
}
else
{
break;
}
}
if(signal == NULL)
{
Print(functionName + ": Failed to initialize Signal after retries");
return INIT_FAILED;
}
InitializeSignal(signal);
// Initializing Database
if(UseDatabaseRanking)
{
bool dbInitialized = false;
string databaseFolderStructure[] = {eaName, "Databases", "Signals"};
//--- fingerprinted so a topology/feature-set change that would produce a differently-shaped or
//--- differently-behaving model gets its own database, instead of silently mixing pattern-weight/
//--- trade-journal history from an incompatible prior config into the one now trading.
const string dbName = Symbol() + "_" + IntegerToString(Period()) + "_" + ComputeDbConfigFingerprint() + ".db";
const string dbVersion = "2.0";
PrintVerbose("Initializing Database...");
for(int tries = 0; !dbInitialized && tries < 5; ++tries)
{
if(!dbm.Init(dbVersion, databaseFolderStructure, dbName))
{
Print(functionName + ": Failed to initialize Database, retrying...");
RandomSleep();
}
else
{
dbInitialized = true;
break;
}
}
if(!dbInitialized)
{
Print(functionName + ": Failed to initialize Database after retries");
return INIT_FAILED;
}
//--- Trade journal shares UseDatabaseRanking's DB connection/lifecycle rather than adding a
//--- second always-on toggle - see Database\TradeJournalManager.mqh's class comment.
if(!journal.Init(GetPointer(dbm), Expert_MagicNumber))
{
Print(functionName + ": Failed to initialize trade journal table");
return INIT_FAILED;
}
}
//+------------------------------------------------------------------+
//| AIType selects which single AI architecture this run trades/ |
//| trains: MLP, CONV, LSTM, or HYBRID. HYBRID is the fused stacked |
//| model, not a 2-of-3 ensemble. |
//| - PAI (CSignalPAI): plain multi-layer Perceptron - input layer |
//| feeds straight into the tapering Dense hidden-layer stack, no |
//| Conv/Pool/LSTM stage. The baseline/cheapest model. |
//| - CONV (CSignalCONV): Conv+Pool front-end ahead of the same |
//| tapering Dense stack - looks for local price-action patterns |
//| (candlestick/short-range shapes) before the dense layers see |
//| them. |
//| - LSTM (CSignalLSTM): a single LSTM layer ahead of the same |
//| tapering Dense stack - genuine forget/input/output-gated |
//| recurrence (see AI\Network.mqh's CNeuronLSTM/CNeuronLSTMOCL), |
//| for sequential/regime-dependent structure the other two can't |
//| see across bars. |
//| HYBRID uses the same AI voting path as the individual models, but |
//| inside one stacked topology: Conv+Pool front-end, then LSTM, then |
//| the common dense taper. Concurrency model: MQL5 is |
//| single-threaded per chart - OnTick()/OnTimer() never run |
//| re-entrantly, so AI signal evaluation never races inside this |
//| EA's own code; PollTraining() below just calls each in turn every |
//| timer tick. The one real concurrency-relevant boundary is the |
//| native compute backend (WarriorCPU.dll/WarriorDML.dll, see |
//| AI\Network.mqh) - each CNet (there can be up to 2 alive at once |
//| per AI signal: live+shadow net) gets its OWN |
//| opaque per-instance context handle with no shared/global DLL |
//| state, so a fault or watchdog-kill against one can never poison |
//| another's calls. Each CNet's WarriorCPU.dll worker pool is sized |
refactor(perf): pin CPU threads per network, drop the TargetCPULoad input Dividing a machine budget by the live chart count was wrong twice over. The count is a snapshot taken when each net's pool is built, and charts attach one at a time: five charts measured 10/6/5/4/4% of the same budget, because the first only ever saw itself and the last saw all five. So the earliest chart got several times the threads of the latest - skewing any cross-topology comparison run on those charts, which is the exact thing the setting existed to make fair. Nothing rebalanced afterwards either, and rebalancing would mean tearing down a DLL context under a live trainer. Both problems disappear once the answer stops depending on how many charts are running. Each net now asks for a fixed 2 worker threads, converted to the percentage the DLL wants from the detected core count. Two is not a compromise: since the topology became data-derived the widest dense layer is 64 units, so each ParallelFor has almost nothing to split and per-dispatch overhead dominates. An MLP era cost ~66s at a wildly oversubscribed 12 threads and ~80s at 1 thread - a 20% spread across a 12x difference in thread count. Two per net also lands six concurrent charts exactly on a 12-core box. Removing the input costs nothing on the product side: a Market build has no DLL tier at all, so it was already compiled out to a constant there and no buyer could reach it. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:33:37 -04:00
//| to a fixed small thread count (CPU_THREADS_PER_NETWORK, |
//| AI\Network.mqh) rather than to a share of the machine, even though |
//| several pools can be alive at once - see that constant's comment. |
//| Within a chart that is free: MQL5's single execution thread per |
//| chart means only one pool is EVER actively computing at a time. |
//| Across charts it is what keeps N charts from each claiming the |
//| whole box, without depending on how many are attached yet. |
//| Memory ownership: AI signal(s) are allocated here with `new` and |
//| registered into g_aiSignals[] for the control panel's benefit, but |
//| g_aiSignals[] does NOT own them - AddFilterToSignal() below adds |
//| each to `signal`'s own filter array (CExpertSignal::AddFilter()), |
//| which frees its elements on destruction; `signal` itself is owned |
//| by Expert (InitializeSignal() -> Expert.InitSignal()). So the |
//| actual free happens via Expert.Deinit() (see OnDeinit() below) |
//| tearing down signal -> its filter array -> the AI signal object, |
//| exactly once - g_aiSignals[] is never delete'd directly (grep |
//| this file has no `delete g_aiSignals` anywhere), so there is no |
//| double-free risk from the two arrays holding the same pointers. |
//+------------------------------------------------------------------+
EnablePAI = (AIType == MLP_3L || AIType == MLP_4L);
EnableCONV = (AIType == CONV_2L);
EnableLSTM = (AIType == LSTM_2L);
EnableHYBRID = (AIType == HYBRID_2L);
// Creating instances of signals
CSignalPAI *PAI = CreateSignalWithRetry<CSignalPAI>(maxRetryOnError, EnablePAI);
CSignalCONV *CONV = CreateSignalWithRetry<CSignalCONV>(maxRetryOnError, EnableCONV);
CSignalLSTM *LSTM = CreateSignalWithRetry<CSignalLSTM>(maxRetryOnError, EnableLSTM);
CSignalHYBRID *HYBRID = CreateSignalWithRetry<CSignalHYBRID>(maxRetryOnError, EnableHYBRID);
//--- register whichever AI signal instances this run created, so the control panel can drive
//--- training/weight actions on exactly this run's current config (never another AIType's files)
g_aiSignalCount = 0;
if(EnablePAI && PAI != NULL)
RegisterAISignal(PAI);
if(EnableCONV && CONV != NULL)
RegisterAISignal(CONV);
if(EnableLSTM && LSTM != NULL)
RegisterAISignal(LSTM);
if(EnableHYBRID && HYBRID != NULL)
RegisterAISignal(HYBRID);
CSignalMA *MA = CreateSignalWithRetry<CSignalMA>(maxRetryOnError, EnableMA);
CSignalRSI *RSI = CreateSignalWithRetry<CSignalRSI>(maxRetryOnError, EnableRSI);
CSignalMACD *MACD = CreateSignalWithRetry<CSignalMACD>(maxRetryOnError, EnableMACD);
CSignalIchimoku *Ichimoku = CreateSignalWithRetry<CSignalIchimoku>(maxRetryOnError, EnableIchimoku);
CSignalNewsFilter *newsFilter = CreateSignalWithRetry<CSignalNewsFilter>(maxRetryOnError, EnableNewsFilter);
CSignalSessionFilter *sessionFilter = CreateSignalWithRetry<CSignalSessionFilter>(maxRetryOnError, EnableSessionFilter);
CSignalITF *ITF = CreateSignalWithRetry<CSignalITF>(maxRetryOnError, EnableITF);
//--- g_marketDepthAvailable is the OnInit-time verified result (see CheckMarketDepthAvailability()
//--- above), not the raw EnableMarketDepth input - only created when real DOM data was confirmed.
CSignalMarketDepth *marketDepth = CreateSignalWithRetry<CSignalMarketDepth>(maxRetryOnError, g_marketDepthAvailable);
CSignalRiskGuard *riskGuard = CreateSignalWithRetry<CSignalRiskGuard>(maxRetryOnError, EnableRiskGuard);
if((EnableITF && ITF == NULL) || (EnableMA && MA == NULL) || (EnableRSI && RSI == NULL) || (EnableMACD && MACD == NULL) || (EnableIchimoku && Ichimoku == NULL) || (EnablePAI && PAI == NULL) || (EnableCONV && CONV == NULL) || (EnableLSTM && LSTM == NULL) || (EnableHYBRID && HYBRID == NULL) || (EnableNewsFilter && newsFilter == NULL) || (EnableSessionFilter && sessionFilter == NULL) || (g_marketDepthAvailable && marketDepth == NULL) || (EnableRiskGuard && riskGuard == NULL))
{
Print("Critical signal initialization failed, cannot proceed");
return INIT_FAILED;
}
// Set filter parameters
if(EnableRiskGuard)
{
riskGuard.SetMaxDailyLossPct(MaxDailyLossPct);
riskGuard.SetMaxDrawdownPct(MaxDrawdownPct);
}
if(EnableSessionFilter)
{
sessionFilter.TradeLondonSession(SF_trade_LondonSession);
sessionFilter.TradeNewYorkSession(SF_trade_NewYorkSession);
sessionFilter.TradeTokyoSession(SF_trade_TokyoSession);
}
if(EnableMA)
{
MA.PeriodMA(PeriodMA);
MA.Method(MA_Type);
if(!UseDatabaseRanking)
MA.Weight(1);
}
if(EnableRSI)
{
RSI.PeriodRSI(PeriodRSI);
if(!UseDatabaseRanking)
RSI.Weight(1);
}
if(EnableMACD)
{
MACD.PeriodFast(MACD_PeriodFast);
MACD.PeriodSlow(MACD_PeriodSlow);
MACD.PeriodSignal(MACD_PeriodSignal);
if(!UseDatabaseRanking)
MACD.Weight(1);
}
if(EnableIchimoku)
{
Ichimoku.PeriodTenkan(Ichimoku_PeriodTenkan);
Ichimoku.PeriodKijun(Ichimoku_PeriodKijun);
Ichimoku.PeriodSenkou(Ichimoku_PeriodSenkou);
if(!UseDatabaseRanking)
Ichimoku.Weight(1);
}
if(EnableITF)
{
ITF.GoodHourOfDay(ITF_GoodHourOfDay);
ITF.BadHoursOfDay(ITF_BadHoursOfDay);
ITF.GoodDayOfWeek(ITF_GoodDayOfWeek);
ITF.BadDaysOfWeek(ITF_BadDaysOfWeek);
}
if(EnableNewsFilter)
{
newsFilter.SetMinImpact(NF_MinImpact);
newsFilter.SetLookbackMinutes(NF_LookMinutes);
}
if(g_marketDepthAvailable)
{
marketDepth.DepthLevels(DOM_DepthLevels);
marketDepth.ImbalanceScale(DOM_ImbalanceScale / 100.0);
marketDepth.MaxSpreadMultiple(DOM_MaxSpreadMultiple);
}
if(EnablePAI)
{
int denseLayers = ResolveDenseTopologyLayers(AIType);
PAI.OutputNeuronsCount(OutputNeuronsCount);
PAI.HiddenLayersCount(denseLayers);
PAI.HistoryBars(ind_Periods);
PAI.StudyPeriod(StudyPeriods);
PAI.MinDirectionalRecall(MinRecall);
PAI.LogitPriorStrength(AILogitPriorStrength / 100.0);
feat(ai): logit-adjusted loss, replacing oversampling and the post-hoc prior Menon et al. 2021 (ICLR), "Long-tail learning via logit adjustment": add tau*log(prior_c) to each class logit inside the training gradient. Softmax CE on adjusted logits is consistent for BALANCED error - the metric checkpoint selection already ranks on - so the loss and the deploy decision finally optimize the same thing. The engine already computed a true softmax + categorical-CE gradient and wrote it over the per-neuron sigmoid delta, so this is an offset added to three logits in the two places that gradient is built (backProp scalar path and backPropOCL). No backend, kernel or DLL change; the forward pass and every inference path are untouched, which is the point - the network learns to absorb the offset, so its raw argmax becomes the balanced-optimal decision with nothing applied at inference. Replaces rather than stacks. Minority replay is disabled while this is on, and the post-hoc inference prior is forced off. Stacking is not a theoretical worry: simulated on the measured 1118/1119/34298 distribution in the weak-signal regime, plain CE collapses to Neutral (33.4% balanced, Buy 0%), replay reaches 48.1%, logit adjustment 50.9% with better balance - and BOTH together score 45.4% with Neutral recall at 0%, worse than either alone. Buda et al. 2018 predicts exactly that. Motivation from the six-chart run: every topology took one direction to ~50% recall and abandoned the other, the direction chosen arbitrarily (the batch-norm control went Buy 1% / Sell 42%, the inverse of the other five). One era in 1,301 cleared the per-class recall floor. Fingerprinted conditionally, so the converged 60.7% models on disk keep their filenames and stay loadable as the fallback. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:05:14 -04:00
PAI.UseLogitAdjustedLoss(EnableLogitAdjustedLoss);
PAI.LogitAdjustTau(LogitAdjustTau / 100.0);
PAI.OversampleParity(OversampleParity / 100.0);
PAI.SignalClusterWindow(SignalClusterWindow);
PAI.FocalLossGamma(FocalLossGamma / 10.0);
PAI.EnableMinorityReplay(EnableMinorityReplay);
PAI.ConstrainReplay(ConstrainReplay);
PAI.FreezePriorCalibration(FreezePriorCalibration);
PAI.UseStaticPrior(UseStaticPrior);
PAI.SwingConfirmationBars(SwingConfirmationBars);
PAI.EnableOnlineLearning(EnableOnlineLearning);
PAI.MaxErasPerRun(MaxErasPerRun);
PAI.OOSSplit(OOSSplit);
if(!UseDatabaseRanking)
PAI.Weight(1);
PAI.UseVolumes(EnableVolume);
PAI.UseTime(EnableTime);
PAI.UseATR(EnableATR);
PAI.UseMA(EnableMAFeature);
PAI.UseRSI(EnableRSIFeature);
PAI.UseMACD(EnableMACDFeature);
PAI.UseIchimoku(EnableIchimokuFeature);
PAI.UseSwingContext(EnableSwingContext);
PAI.UseNews(EnableNews);
PAI.NewsFeatureWindowMinutes(NewsFeatureWindowMinutes);
PAI.UseADCumulativeDelta(EnableADCumulativeDelta);
PAI.UseADShorteningOfThrust(EnableADShorteningOfThrust);
PAI.UseADWyckoffEventStream(EnableADWyckoffEventStream);
PAI.UseADWyckoffFailedStructure(EnableADWyckoffFailedStructure);
PAI.UseADWyckoffSignificantBarInversion(EnableADWyckoffSignificantBarInversion);
PAI.AutoTuneIndicators(AutoTuneIndicators);
PAI.IndicatorTuneTrials(IndicatorTuneTrials);
}
if(EnableCONV)
{
int denseLayers = ResolveDenseTopologyLayers(AIType);
CONV.OutputNeuronsCount(OutputNeuronsCount);
CONV.HiddenLayersCount(denseLayers);
CONV.ConvFilterCount(ConvFilterCount);
CONV.HistoryBars(ind_Periods);
CONV.StudyPeriod(StudyPeriods);
CONV.MinDirectionalRecall(MinRecall);
CONV.LogitPriorStrength(AILogitPriorStrength / 100.0);
feat(ai): logit-adjusted loss, replacing oversampling and the post-hoc prior Menon et al. 2021 (ICLR), "Long-tail learning via logit adjustment": add tau*log(prior_c) to each class logit inside the training gradient. Softmax CE on adjusted logits is consistent for BALANCED error - the metric checkpoint selection already ranks on - so the loss and the deploy decision finally optimize the same thing. The engine already computed a true softmax + categorical-CE gradient and wrote it over the per-neuron sigmoid delta, so this is an offset added to three logits in the two places that gradient is built (backProp scalar path and backPropOCL). No backend, kernel or DLL change; the forward pass and every inference path are untouched, which is the point - the network learns to absorb the offset, so its raw argmax becomes the balanced-optimal decision with nothing applied at inference. Replaces rather than stacks. Minority replay is disabled while this is on, and the post-hoc inference prior is forced off. Stacking is not a theoretical worry: simulated on the measured 1118/1119/34298 distribution in the weak-signal regime, plain CE collapses to Neutral (33.4% balanced, Buy 0%), replay reaches 48.1%, logit adjustment 50.9% with better balance - and BOTH together score 45.4% with Neutral recall at 0%, worse than either alone. Buda et al. 2018 predicts exactly that. Motivation from the six-chart run: every topology took one direction to ~50% recall and abandoned the other, the direction chosen arbitrarily (the batch-norm control went Buy 1% / Sell 42%, the inverse of the other five). One era in 1,301 cleared the per-class recall floor. Fingerprinted conditionally, so the converged 60.7% models on disk keep their filenames and stay loadable as the fallback. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:05:14 -04:00
CONV.UseLogitAdjustedLoss(EnableLogitAdjustedLoss);
CONV.LogitAdjustTau(LogitAdjustTau / 100.0);
CONV.OversampleParity(OversampleParity / 100.0);
CONV.SignalClusterWindow(SignalClusterWindow);
CONV.FocalLossGamma(FocalLossGamma / 10.0);
CONV.EnableMinorityReplay(EnableMinorityReplay);
CONV.ConstrainReplay(ConstrainReplay);
CONV.FreezePriorCalibration(FreezePriorCalibration);
CONV.UseStaticPrior(UseStaticPrior);
CONV.SwingConfirmationBars(SwingConfirmationBars);
CONV.EnableOnlineLearning(EnableOnlineLearning);
CONV.MaxErasPerRun(MaxErasPerRun);
CONV.OOSSplit(OOSSplit);
if(!UseDatabaseRanking)
CONV.Weight(1);
CONV.UseVolumes(EnableVolume);
CONV.UseTime(EnableTime);
CONV.UseATR(EnableATR);
CONV.UseMA(EnableMAFeature);
CONV.UseRSI(EnableRSIFeature);
CONV.UseMACD(EnableMACDFeature);
CONV.UseIchimoku(EnableIchimokuFeature);
CONV.UseSwingContext(EnableSwingContext);
CONV.UseNews(EnableNews);
CONV.NewsFeatureWindowMinutes(NewsFeatureWindowMinutes);
CONV.UseADCumulativeDelta(EnableADCumulativeDelta);
CONV.UseADShorteningOfThrust(EnableADShorteningOfThrust);
CONV.UseADWyckoffEventStream(EnableADWyckoffEventStream);
CONV.UseADWyckoffFailedStructure(EnableADWyckoffFailedStructure);
CONV.UseADWyckoffSignificantBarInversion(EnableADWyckoffSignificantBarInversion);
CONV.AutoTuneIndicators(AutoTuneIndicators);
CONV.IndicatorTuneTrials(IndicatorTuneTrials);
}
if(EnableLSTM)
{
int denseLayers = ResolveDenseTopologyLayers(AIType);
LSTM.OutputNeuronsCount(OutputNeuronsCount);
LSTM.HiddenLayersCount(denseLayers);
LSTM.LstmHiddenSize(LstmHiddenSize);
LSTM.HistoryBars(ind_Periods);
LSTM.StudyPeriod(StudyPeriods);
LSTM.MinDirectionalRecall(MinRecall);
LSTM.LogitPriorStrength(AILogitPriorStrength / 100.0);
feat(ai): logit-adjusted loss, replacing oversampling and the post-hoc prior Menon et al. 2021 (ICLR), "Long-tail learning via logit adjustment": add tau*log(prior_c) to each class logit inside the training gradient. Softmax CE on adjusted logits is consistent for BALANCED error - the metric checkpoint selection already ranks on - so the loss and the deploy decision finally optimize the same thing. The engine already computed a true softmax + categorical-CE gradient and wrote it over the per-neuron sigmoid delta, so this is an offset added to three logits in the two places that gradient is built (backProp scalar path and backPropOCL). No backend, kernel or DLL change; the forward pass and every inference path are untouched, which is the point - the network learns to absorb the offset, so its raw argmax becomes the balanced-optimal decision with nothing applied at inference. Replaces rather than stacks. Minority replay is disabled while this is on, and the post-hoc inference prior is forced off. Stacking is not a theoretical worry: simulated on the measured 1118/1119/34298 distribution in the weak-signal regime, plain CE collapses to Neutral (33.4% balanced, Buy 0%), replay reaches 48.1%, logit adjustment 50.9% with better balance - and BOTH together score 45.4% with Neutral recall at 0%, worse than either alone. Buda et al. 2018 predicts exactly that. Motivation from the six-chart run: every topology took one direction to ~50% recall and abandoned the other, the direction chosen arbitrarily (the batch-norm control went Buy 1% / Sell 42%, the inverse of the other five). One era in 1,301 cleared the per-class recall floor. Fingerprinted conditionally, so the converged 60.7% models on disk keep their filenames and stay loadable as the fallback. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:05:14 -04:00
LSTM.UseLogitAdjustedLoss(EnableLogitAdjustedLoss);
LSTM.LogitAdjustTau(LogitAdjustTau / 100.0);
LSTM.OversampleParity(OversampleParity / 100.0);
LSTM.SignalClusterWindow(SignalClusterWindow);
LSTM.FocalLossGamma(FocalLossGamma / 10.0);
LSTM.EnableMinorityReplay(EnableMinorityReplay);
LSTM.ConstrainReplay(ConstrainReplay);
LSTM.FreezePriorCalibration(FreezePriorCalibration);
LSTM.UseStaticPrior(UseStaticPrior);
LSTM.SwingConfirmationBars(SwingConfirmationBars);
LSTM.EnableOnlineLearning(EnableOnlineLearning);
LSTM.MaxErasPerRun(MaxErasPerRun);
LSTM.OOSSplit(OOSSplit);
if(!UseDatabaseRanking)
LSTM.Weight(1);
LSTM.UseVolumes(EnableVolume);
LSTM.UseTime(EnableTime);
LSTM.UseATR(EnableATR);
LSTM.UseMA(EnableMAFeature);
LSTM.UseRSI(EnableRSIFeature);
LSTM.UseMACD(EnableMACDFeature);
LSTM.UseIchimoku(EnableIchimokuFeature);
LSTM.UseSwingContext(EnableSwingContext);
LSTM.UseNews(EnableNews);
LSTM.NewsFeatureWindowMinutes(NewsFeatureWindowMinutes);
LSTM.UseADCumulativeDelta(EnableADCumulativeDelta);
LSTM.UseADShorteningOfThrust(EnableADShorteningOfThrust);
LSTM.UseADWyckoffEventStream(EnableADWyckoffEventStream);
LSTM.UseADWyckoffFailedStructure(EnableADWyckoffFailedStructure);
LSTM.UseADWyckoffSignificantBarInversion(EnableADWyckoffSignificantBarInversion);
LSTM.AutoTuneIndicators(AutoTuneIndicators);
LSTM.IndicatorTuneTrials(IndicatorTuneTrials);
}
if(EnableHYBRID)
{
int denseLayers = ResolveDenseTopologyLayers(AIType);
HYBRID.OutputNeuronsCount(OutputNeuronsCount);
HYBRID.HiddenLayersCount(denseLayers);
HYBRID.ConvFilterCount(ConvFilterCount);
HYBRID.LstmHiddenSize(LstmHiddenSize);
HYBRID.HistoryBars(ind_Periods);
HYBRID.StudyPeriod(StudyPeriods);
HYBRID.MinDirectionalRecall(MinRecall);
HYBRID.LogitPriorStrength(AILogitPriorStrength / 100.0);
feat(ai): logit-adjusted loss, replacing oversampling and the post-hoc prior Menon et al. 2021 (ICLR), "Long-tail learning via logit adjustment": add tau*log(prior_c) to each class logit inside the training gradient. Softmax CE on adjusted logits is consistent for BALANCED error - the metric checkpoint selection already ranks on - so the loss and the deploy decision finally optimize the same thing. The engine already computed a true softmax + categorical-CE gradient and wrote it over the per-neuron sigmoid delta, so this is an offset added to three logits in the two places that gradient is built (backProp scalar path and backPropOCL). No backend, kernel or DLL change; the forward pass and every inference path are untouched, which is the point - the network learns to absorb the offset, so its raw argmax becomes the balanced-optimal decision with nothing applied at inference. Replaces rather than stacks. Minority replay is disabled while this is on, and the post-hoc inference prior is forced off. Stacking is not a theoretical worry: simulated on the measured 1118/1119/34298 distribution in the weak-signal regime, plain CE collapses to Neutral (33.4% balanced, Buy 0%), replay reaches 48.1%, logit adjustment 50.9% with better balance - and BOTH together score 45.4% with Neutral recall at 0%, worse than either alone. Buda et al. 2018 predicts exactly that. Motivation from the six-chart run: every topology took one direction to ~50% recall and abandoned the other, the direction chosen arbitrarily (the batch-norm control went Buy 1% / Sell 42%, the inverse of the other five). One era in 1,301 cleared the per-class recall floor. Fingerprinted conditionally, so the converged 60.7% models on disk keep their filenames and stay loadable as the fallback. Both builds compile 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:05:14 -04:00
HYBRID.UseLogitAdjustedLoss(EnableLogitAdjustedLoss);
HYBRID.LogitAdjustTau(LogitAdjustTau / 100.0);
HYBRID.OversampleParity(OversampleParity / 100.0);
HYBRID.SignalClusterWindow(SignalClusterWindow);
HYBRID.FocalLossGamma(FocalLossGamma / 10.0);
HYBRID.EnableMinorityReplay(EnableMinorityReplay);
HYBRID.ConstrainReplay(ConstrainReplay);
HYBRID.FreezePriorCalibration(FreezePriorCalibration);
HYBRID.UseStaticPrior(UseStaticPrior);
HYBRID.SwingConfirmationBars(SwingConfirmationBars);
HYBRID.EnableOnlineLearning(EnableOnlineLearning);
HYBRID.MaxErasPerRun(MaxErasPerRun);
HYBRID.OOSSplit(OOSSplit);
if(!UseDatabaseRanking)
HYBRID.Weight(1);
HYBRID.UseVolumes(EnableVolume);
HYBRID.UseTime(EnableTime);
HYBRID.UseATR(EnableATR);
HYBRID.UseMA(EnableMAFeature);
HYBRID.UseRSI(EnableRSIFeature);
HYBRID.UseMACD(EnableMACDFeature);
HYBRID.UseIchimoku(EnableIchimokuFeature);
HYBRID.UseSwingContext(EnableSwingContext);
HYBRID.UseNews(EnableNews);
HYBRID.NewsFeatureWindowMinutes(NewsFeatureWindowMinutes);
HYBRID.UseADCumulativeDelta(EnableADCumulativeDelta);
HYBRID.UseADShorteningOfThrust(EnableADShorteningOfThrust);
HYBRID.UseADWyckoffEventStream(EnableADWyckoffEventStream);
HYBRID.UseADWyckoffFailedStructure(EnableADWyckoffFailedStructure);
HYBRID.UseADWyckoffSignificantBarInversion(EnableADWyckoffSignificantBarInversion);
HYBRID.AutoTuneIndicators(AutoTuneIndicators);
HYBRID.IndicatorTuneTrials(IndicatorTuneTrials);
}
// Add filters
PrintVerbose("Initializing Signal filters...");
//--- added exactly once, before the DB retry loop below - these calls don't depend on DB success at
//--- all (every pointer here was already validated non-NULL above), but living inside the loop body
//--- meant a DB open/transaction failure that triggered a retry would re-run AddFilterToSignal() and
//--- register the same filter pointer a second time in signal's CArrayObj; since that array frees its
//--- elements on destruction, a duplicate entry means the same pointer gets deleted twice on shutdown
//--- (heap corruption), which could easily explain instability across a later remove/re-add cycle.
bool filtersAdded = true;
filtersAdded &= (EnableMA ? AddFilterToSignal(signal, MA) : true);
filtersAdded &= (EnableRSI ? AddFilterToSignal(signal, RSI) : true);
filtersAdded &= (EnableMACD ? AddFilterToSignal(signal, MACD) : true);
filtersAdded &= (EnableIchimoku ? AddFilterToSignal(signal, Ichimoku) : true);
filtersAdded &= (EnableITF ? AddFilterToSignal(signal, ITF) : true);
filtersAdded &= (EnableSessionFilter ? AddFilterToSignal(signal, sessionFilter) : true);
filtersAdded &= (g_marketDepthAvailable ? AddFilterToSignal(signal, marketDepth) : 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(functionName + ": Error loading filters");
return INIT_FAILED;
}
bool filterSuccess = false;
for(int tries = 0; tries < maxRetryOnError; ++tries)
{
if(UseDatabaseRanking && !dbm.OpenDatabase())
{
Print(functionName + ": Error opening database, retrying...");
RandomSleep();
continue;
}
if(UseDatabaseRanking && !dbm.BeginTransaction())
{
Print(functionName + ": Error starting transaction, retrying...");
dbm.CloseDatabase(); // Ensure the database is closed before retry
RandomSleep();
continue;
}
if(UseDatabaseRanking && (!dbm.CommitTransaction() || !dbm.CloseDatabase()))
{
Print(functionName + ": Error committing transaction or closing database, retrying...");
RandomSleep();
continue;
}
filterSuccess = true;
break; // Success if all operations complete without error
}
if(!filterSuccess)
{
Print(functionName + ": Failed after all retries");
return INIT_FAILED; // Return failure if retries are exhausted
}
// Trailing logic
PrintVerbose("Initializing Trailing...");
bool trailingInitialized = false;
for(int tries = 0; !trailingInitialized && tries < maxRetryOnError; ++tries)
{
if(!InitializeTrailing())
{
Print(functionName + ": Failed to initialize Trailing, retrying...");
RandomSleep();
}
else
{
trailingInitialized = true;
break;
}
}
if(!trailingInitialized)
{
Print(functionName + ": Failed to initialize Trailing after retries");
return INIT_FAILED;
}
// Creation of money object
bool moneyManagementInitialized = false;
for(int tries = 0; !moneyManagementInitialized && tries < maxRetryOnError; ++tries)
{
if(!InitializeMoneyManagement())
{
Print(functionName + ": Failed to initialize Money Management, retrying...");
RandomSleep();
}
else
{
moneyManagementInitialized = true;
break;
}
}
if(!moneyManagementInitialized)
{
Print(functionName + ": Failed to initialize Money Management after retries");
return INIT_FAILED;
}
// Check all trading objects parameters
PrintVerbose("Validating settings...");
bool settingsValidated = false;
for(int tries = 0; !settingsValidated && tries < maxRetryOnError; ++tries)
{
if(!Expert.ValidationSettings())
{
Print(functionName + ": Failed to validate settings, retrying...");
RandomSleep();
}
else
{
settingsValidated = true;
break;
}
}
if(!settingsValidated)
{
Print(functionName + ": Failed to validate settings after retries");
return INIT_FAILED;
}
// Tuning of all necessary indicators
PrintVerbose("Initializing Indicators...");
bool indicatorsInitialized = false;
for(int tries = 0; !indicatorsInitialized && tries < maxRetryOnError; ++tries)
{
if(!Expert.InitIndicators())
{
Print(functionName + ": Failed to initialize Indicators, retrying...");
RandomSleep();
}
else
{
indicatorsInitialized = true;
break;
}
}
if(!indicatorsInitialized)
{
Print(functionName + ": Failed to initialize Indicators after retries");
return INIT_FAILED;
}
// setting timer: always on (short interval) so control-panel upkeep and other periodic checks run
// on a fixed schedule regardless of tick activity - a quiet/after-hours symbol can go long stretches
// without a single OnTick() call, and self-healing logic that only lives in OnTick() would never run
// during that stretch. The (much less frequent) DB-ranking work is throttled separately inside
// OnTimer() below rather than by EventSetTimer's own interval, since MQL5 only supports one timer
// interval per program.
if(UseDatabaseRanking)
Expert.OnTimerProcess(true);
bool timerSet = false;
// 500ms: Train() only does up to TRAIN_TIME_BUDGET_MS (80ms) of work per call, then yields back
// here. This interval drives training only on quiet symbols / no-tick stretches (real tick flow
// drives it independently), so it just needs to be short enough that a quiet era doesn't crawl -
// it was 5s originally (era took ~2min for <2s of compute), then 250ms. 250ms made the on-chart
// control panel laggy/hard to drag: every 250ms the timer ran ~80ms of compute AND a ChartRedraw,
// which competed with the user's drag events. 500ms halves that redraw/compute contention (smooth
// drag) while a quiet era still advances at ~80ms busy / 500ms = a healthy duty cycle. EventSet-
// MillisecondTimer is needed for sub-second resolution; EventSetTimer only accepts whole seconds.
// DB-ranking work below is paced by its own g_lastDbRankingRun/DB_RANKING_INTERVAL_SECONDS check,
// not by this interval, so it still only runs ~hourly regardless of this change.
int timerInterval_ms = 500;
for(int tries = 0; !timerSet && tries < maxRetryOnError; ++tries)
{
if(!EventSetMillisecondTimer(timerInterval_ms))
{
Print(functionName + ": Error creating timer, retrying...");
RandomSleep();
}
else
{
timerSet = true;
break;
}
}
if(!timerSet)
{
Print(functionName + ": Failed to set timer after retries");
return INIT_FAILED;
}
// Initialization successful
PrintVerbose("Initialization successful");
if(!CreateControlPanel())
Print(functionName + ": WARNING - control panel failed to initialize; trading/training continue normally, "
"but no GUI panel will be available for this run");
//--- one-time cleanup: an earlier build used Comment() plus a separate background rectangle object
//--- that turned out to render ON TOP of the text (Comment() has no built-in background/styling
//--- parameters at all) - delete any leftover from a prior run now that status text is a single
//--- self-contained OBJ_LABEL (see SetStatusLabel()) with its own BGCOLOR fill instead
if(ObjectFind(0, "WarriorCommentBG") >= 0)
ObjectDelete(0, "WarriorCommentBG");
//--- required for CAppDialog's caption-bar drag to work at all - without it, the chart never delivers
//--- CHARTEVENT_MOUSE_MOVE and the panel silently ignores drag attempts
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
g_lastAlgoTradingAllowed = (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && (bool)MQLInfoInteger(MQL_TRADE_ALLOWED);
if(!g_lastAlgoTradingAllowed)
Print(functionName + ": WARNING - AlgoTrading is currently disabled (terminal or EA); signals will still train but no orders will be sent until it is re-enabled");
return INIT_SUCCEEDED;
}
// Called before optimization/backtesting in the strategy tester
int OnTesterInit()
{
IsBacktesting = true;
return(INIT_SUCCEEDED);
}
// Called after EA optimization in the strategy tester
void OnTesterDeinit()
{
// OnTesterInit/OnTesterDeinit fire once per tester session, even for a single (non-optimization)
// backtest - and by the time this runs, that pass's own OnDeinit() has ALREADY executed (the
// terminal calls it automatically at the end of every pass), including its own dbm.Deinit().
// Calling dbm.Deinit()/OnDeinit(0) again here duplicated the whole shutdown path (panel/arrow
// cleanup, weight save, MarketBookRelease) a second time with a hardcoded/wrong reason code -
// just reset the tester-only flag here.
IsBacktesting = false;
}
//+------------------------------------------------------------------+
//| Tester function for smooth linear equity optimization |
//| Output Range: 0.0 (Worst/Failed) to 100.0 (Perfect Linear Curve) |
//+------------------------------------------------------------------+
double OnTester()
{
// 1. Enforce minimum performance thresholds
double totalTrades = TesterStatistics(STAT_TRADES);
if(totalTrades < 30) return(0.0);
double netProfit = TesterStatistics(STAT_PROFIT);
if(netProfit <= 0) return(0.0);
double maxDrawdownPct = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
if(maxDrawdownPct > 15.0) return(0.0);
// 2. Extract key performance components for linearity proxy
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double recoveryFactor = TesterStatistics(STAT_RECOVERY_FACTOR);
double sharpeRatio = TesterStatistics(STAT_SHARPE_RATIO);
if(profitFactor <= 0 || recoveryFactor <= 0) return(0.0);
if(sharpeRatio < 0) sharpeRatio = 0.01;
// 3. Trade density multiplier
double tradeDensity = 1.0 - MathExp(-0.01 * (double)totalTrades);
// 4. Mathematical combination proxy targeting visual linearity
double rawScore = profitFactor * recoveryFactor * sharpeRatio;
// FIX: Convert percent to decimal (divide by 100) before penalization
double drawdownDecimal = maxDrawdownPct / 100.0;
rawScore /= (1.0 + (drawdownDecimal * 0.5));
// Apply trade density
rawScore *= tradeDensity;
// 5. Normalize the score to a 0.0 - 100.0 scale
double finalScore = 100.0 * (1.0 - MathExp(-0.12 * rawScore));
if(finalScore > 100.0) finalScore = 100.0;
if(finalScore < 0.0) finalScore = 0.0;
return(finalScore);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Maps a terminal deinit reason code to a short label for logging, |
//| so operators can tell a routine recompile/parameter change apart |
//| from a terminal shutdown or the EA actually being removed. |
//+------------------------------------------------------------------+
string DeinitReasonToString(const int reason)
{
switch(reason)
{
case REASON_PROGRAM: return "EA stopped by ExpertRemove()/self";
case REASON_REMOVE: return "EA removed from chart";
case REASON_RECOMPILE: return "EA recompiled";
case REASON_CHARTCHANGE: return "chart symbol/period changed";
case REASON_CHARTCLOSE: return "chart closed";
case REASON_PARAMETERS: return "input parameters changed";
case REASON_ACCOUNT: return "account changed";
case REASON_TEMPLATE: return "template applied";
case REASON_INITFAILED: return "OnInit() failed";
case REASON_CLOSE: return "terminal closed";
default: return "unknown (" + IntegerToString(reason) + ")";
}
}
void OnDeinit(const int reason)
{
static bool s_deinitInProgress = false;
if(s_deinitInProgress)
return;
s_deinitInProgress = true;
// Stop timer callbacks first so no more periodic work is queued while teardown runs.
EventKillTimer();
string reasonStr = DeinitReasonToString(reason);
Print(__FUNCTION__ + ": shutting down - reason: " + reasonStr);
bool isTesterRun = (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD));
if(!isTesterRun)
SaveSignalsVisibilityState();
bool preserveChartArrows = (reason == REASON_RECOMPILE || reason == REASON_PARAMETERS ||
reason == REASON_CHARTCHANGE || reason == REASON_TEMPLATE);
if(preserveChartArrows)
Print(__FUNCTION__ + ": preserving signal arrows on chart for warm reload reason.");
//--- Clear the VISIBLE EA objects FIRST and fast - status label, then control panel, then arrows -
//--- BEFORE the slow/fragile weight save below. On the CPU-DLL box the recursive Net.Save (two full nets
//--- per signal) and the net teardown in Expert.Deinit() can run long enough for MT5 to force-terminate
//--- OnDeinit ("Abnormal termination"); whatever was ordered AFTER that point never ran, which is what
//--- left the status label stuck on the chart. ClearStatusLabel() is a single fast op and goes ABSOLUTELY
//--- first (it was the reported straggler) so it happens even if a later step stalls; the panel and arrow
//--- purge follow while still cheap. Only then the heavy persistence runs, best-effort, last.
ClearStatusLabel();
//--- Destroy the control panel's own UI before object teardown so CAppDialog removes its own objects
//--- cleanly (see Controls\Dialog.mqh) rather than racing the purges below.
if(!isTesterRun)
ExtPanel.Destroy(reason);
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].StopTraining();
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].ShutdownChartCleanup(preserveChartArrows); // persist arrows, optionally keep them visible on warm reload
//--- matches the MarketBookAdd() in CheckMarketDepthAvailability() (OnInit) - released only if subscribed.
if(g_marketDepthAvailable)
MarketBookRelease(_Symbol);
//--- Persist every active AI signal's current in-memory weights/state, so a terminal restart, recompile,
//--- chart re-add or template swap resumes from here rather than from the last fully-completed training
//--- era only. Best-effort and LAST: if it stalls/faults the chart is already clean (cleanup ran above)
//--- AND the last completed era is already on disk (every era end and the periodic autosave both persist
//--- independently), and CNet::Save is atomic so a killed write can never damage the existing .nnw.
//--- NOTE: an earlier revision skipped this entirely for in-process reloads (REASON_RECOMPILE /
//--- CHARTCHANGE / PARAMETERS) on the theory that an over-budget save left the CPU-DLL holding the old
//--- net's tensors and made the reload fail to allocate layer 0. That theory was wrong - the real cause of
//--- "loaded 0 of N layers (failed at layer 0)" was CLayer::CreateElement no longer overriding
//--- CArrayObj::CreateElement, so the read path failed before touching any backend (see AI\Network.mqh).
//--- With that fixed there is no reason to throw away the in-progress era on every recompile.
if(!isTesterRun)
{
for(int i = 0; i < g_aiSignalCount; i++)
{
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 itself runs every 5s (see OnInit), 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;
void OnTimer()
{
//--- 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();
//--- Finish the Show Signals sequence once every instance queued by ToggleSignalsVisibility has
//--- drained its chunked rescan (each is advanced one slice per PollTraining call above).
FinalizeSignalsRescanIfDone();
//--- Training can finish and deploy ITSELF (the plateau ladder finalising the best checkpoint, or the
//--- era-cap deploy) with no button ever pressed, and RefreshControlPanelLabels() otherwise only runs
//--- in response to a click - which would leave the panel offering "Deploy Model" on an
//--- already-deployed model until the user happened to click something. Watch for the transition and
//--- resync once, rather than repainting every 500ms tick for nothing.
bool deployedNow = AllTrainingDeployed();
if(deployedNow != g_lastDeployedState)
{
g_lastDeployedState = deployedNow;
RefreshControlPanelLabels();
}
if(!UseDatabaseRanking)
return;
datetime now = TimeCurrent();
if(g_lastDbRankingRun != 0 && now - g_lastDbRankingRun < DB_RANKING_INTERVAL_SECONDS)
return;
g_lastDbRankingRun = now;
Expert.OnTimer();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CheckAlgoTradingState(void)
{
bool allowed = (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && (bool)MQLInfoInteger(MQL_TRADE_ALLOWED);
if(allowed != g_lastAlgoTradingAllowed)
{
if(allowed)
Print(__FUNCTION__ + ": AlgoTrading re-enabled - order placement resumed (training/signals were unaffected while disabled)");
else
Print(__FUNCTION__ + ": AlgoTrading disabled (terminal toggle off, or EA's own permission revoked) - no new orders will be sent until re-enabled; training/signal generation continues unaffected");
g_lastAlgoTradingAllowed = allowed;
}
}
void AutosaveWeightsIfDue(void)
{
//--- NEVER autosave inside the Strategy Tester / optimizer. The whole reason this exists is that a live
//--- terminal can be killed without OnDeinit running - a tester run has no such exposure. Worse, "new
//--- bar" in the tester means every SIMULATED bar, so this fired a full ~7MB model write per simulated
//--- hour (thousands of writes per backtest), spamming save failures and burning the entire run's time
//--- on I/O. A tester run is inference-only anyway (see m_inferenceOnly) - the weights never change, so
//--- there is literally nothing to persist.
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return;
datetime lastBarDate = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
//--- <=0 is a transient history-sync hiccup, not "no new bar" - skip this tick and try again on the
//--- next one rather than risk locking onto a bad watermark (same guard philosophy as
//--- ScheduleTrainingIfNeeded's own lastBarDate read).
if(lastBarDate <= 0 || lastBarDate == g_lastAutosaveBarTime)
return;
g_lastAutosaveBarTime = lastBarDate;
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].SaveWeightsNow();
}
void OnTick()
{
CheckAlgoTradingState();
AutosaveWeightsIfDue();
Expert.OnTick();
if(UseDatabaseRanking)
journal.Update();
//--- new arrows are always created visible; if signals are currently hidden, re-hide
//--- any that were drawn this tick (cheap - only runs while the toggle is in the "hidden" state)
if(!g_signalsVisible)
ApplySignalsVisibility();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
//--- canonical CAppDialog usage (Controls\Dialog.mqh): forward every event to the dialog first, since
//--- that's what drives its own click/drag hit-testing (via CHARTEVENT_MOUSE_MOVE) as well as our
//--- buttons' EVENT_MAP handlers (see ControlPanel.mqh) - then pick up whatever button action, if any,
//--- that just recorded.
ExtPanel.ChartEvent(id, lparam, dparam, sparam);
HandleControlPanelAction(ExtPanel.ConsumeAction());
Expert.OnChartEvent(id, lparam, dparam, sparam);
//--- 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);
signal_obj.Periods(ind_Periods);
signal_obj.SLMode((int)SL_Mode);
signal_obj.TPMode((int)TP_Mode);
signal_obj.MinRiskRewardRatio(Min_Risk_Reward_Ratio);
signal_obj.ConfidenceSource((int)Confidence_Source);
//--- Gates AddFilter()'s DB pattern-table creation and Direction()'s per-tick DB signal buffering
//--- (ExpertSignalCustom.mqh:286/555) - without this, UseDatabaseRanking only skipped the Weight(1)
//--- default below and never actually populated the win-rate tables UpdateSignalsWeights() reads from.
signal_obj.UseDatabase(UseDatabaseRanking);
//--- Same two inputs feed BOTH engines, and now on ONE scale each. Open is purely the averaged-vote
//--- threshold - there is no separate AI entry floor any more (the AI expresses its confidence as its
//--- vote weight instead, see CExpertSignalAIBase::ConfidenceTier/m_pattern_0), so no
//--- MinSignalConfidence() call is set on the AI signals above. Close drives the rule-based exit and
//--- the AI early exit. See Min_Vote_Open's declaration comment (Variables\Inputs.mqh) for the shared
//--- 0-100 conviction scale, and for why open and close must stay independent of each other.
//--- No UseAIExit() call any more - the early-exit route has no separate on/off switch, because
//--- Min_Vote_Close = Disabled already lands here as 1.01 and there disables it by arithmetic.
signal_obj.AIExitThreshold(Min_Vote_Close / 100.0);
signal_obj.ThresholdOpen((int)Min_Vote_Open);
signal_obj.ThresholdClose((int)Min_Vote_Close);
//--- HYBRID is now one fused signal, so no separate AI quorum is needed here.
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
// Initialize Trailing
bool InitializeTrailing()
{
if(TrailingStrategy == TRAILING_STRATEGY_NONE)
{
// No trailing strategy selected
return true;
}
else
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x1 ||
TrailingStrategy == TRAILING_STRATEGY_ATR_x2 ||
TrailingStrategy == TRAILING_STRATEGY_ATR_x3)
{
// ATR Trailing Strategy
double multiplier = 0;
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x1)
multiplier = 1;
else
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x2)
multiplier = 2;
else
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x3)
multiplier = 3;
CTrailingATR *trailing = new CTrailingATR;
if(trailing == NULL)
{
Print(__FUNCTION__ + ": error creating trailing");
return false;
}
// Set ATR Multiplier
trailing.Multiplier(multiplier);
if(!Expert.InitTrailing(trailing))
{
Print(__FUNCTION__ + ": error initializing trailing");
return false;
}
}
else
if(TrailingStrategy == TRAILING_STRATEGY_INTELLIGENT)
{
// Confidence-adaptive ATR trailing (see Trailing\TrailingIntelligent.mqh) - the ATR
// multiple is set per-check from live AI confidence, so no fixed Multiplier() here.
CTrailingIntelligent *trailing = new CTrailingIntelligent;
if(trailing == NULL)
{
Print(__FUNCTION__ + ": error creating intelligent trailing");
return false;
}
if(!Expert.InitTrailing(trailing))
{
Print(__FUNCTION__ + ": error initializing intelligent trailing");
return false;
}
}
// Add more trailing strategies if needed
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool InitializeMoneyManagement()
{
string functionName = __FUNCTION__;
if(MM_STRATEGY == FIXED_RISK)
{
CMoneyFixedRisk *money = new CMoneyFixedRisk;
if(money == NULL)
{
Print(functionName + ": error creating money");
return false;
}
if(!Expert.InitMoney(money))
{
Print(functionName + ": error initializing money");
return false;
}
money.Percent(Money_Risk_Percent);
}
else
if(MM_STRATEGY == FIXED_LOT)
{
CMoneyFixedLot *money = new CMoneyFixedLot;
if(money == NULL)
{
Print(functionName + ": error creating money");
return false;
}
if(!Expert.InitMoney(money))
{
Print(functionName + ": error initializing money");
return false;
}
money.Lots(Money_FixLot_Lots);
}
else
if(MM_STRATEGY == INTELLIGENT)
{
CMoneyIntelligent *money = new CMoneyIntelligent;
if(money == NULL)
{
Print(functionName + ": error creating money");
return false;
}
if(!Expert.InitMoney(money))
{
Print(functionName + ": error initializing money");
return false;
}
money.Percent(Money_Risk_Percent);
//--- Intelligent MM is AI-driven by definition now (the old Use_AI_Lot_Sizing toggle was
//--- removed as redundant) - always scale risk% by confidence via the Kelly path.
money.UseAIConfidenceLotSizing(true);
money.ConfidenceSource((int)Confidence_Source);
}
// Add more money management strategies if needed
return true;
}
//+------------------------------------------------------------------+