MQL5 ONNX Regime Detection EA: Adaptive Strategy Switching

Build an MQL5 Expert Advisor that detects market regimes using ONNX machine learning models. Train a classifier offline, load it into MetaTrader 5, and switch.

mql5-onnx-regime-detection-ea-adaptive-strategy

Why You Need Regime Detection in Your EA

Most Expert Advisors fail not because the logic is wrong, but because the market changed. A trend-following EA that crushes it in a strong uptrend gets destroyed in a range. A mean-reversion scalper works beautifully in sideways markets, but misses the boat—or gets stopped out repeatedly—when a trend kicks in. You've seen this happen: you optimize an EA on six months of data, it looks golden, then the next month the regime flips and your equity curve goes vertical—downward.

The solution isn't a single strategy that tries to work everywhere. It's an EA that detects the current regime and adapts. That's where machine learning and MQL5's ONNX runtime come together. Instead of hardcoding rules like "if ADX > 25 then trend," you train a model to classify market states from a richer set of features. The EA loads that model, makes a prediction every bar, and switches between strategy modules accordingly.

I've been down this road with hand-coded logic for years—multiple timeframe ADX, Bollinger Band squeeze detection, volatility ratios. They work, but they break in edge cases. A well-trained ONNX model generalizes better. Let me show you how to build this from the ground up.

One thing most traders overlook: regime detection isn't just about avoiding losses. It's about capital efficiency. When you know the market is trending, you can size up and let profits run. When it's ranging, you tighten stops and take quicker profits. When volatility spikes, you cut position size to survive the whipsaw. A single static strategy can't do that. This EA can.

I've seen too many traders throw a neural network at price prediction and wonder why it overfits. Regime detection sidesteps that trap entirely. You're not predicting where price goes—you're classifying the type of market you're in. That's a fundamentally easier problem, and it gives you a lever to adjust risk and strategy parameters without guessing exact entries.

What Is an ONNX Regime Detection EA?

ONNX (Open Neural Network Exchange) is a format for machine learning models. MQL5 added the OnnxRuntime library in build 3250, letting you load and run ONNX models directly inside an EA—no external Python server, no DLL. You train the model offline in Python (or any ML framework), export it to .onnx, and the EA uses it to classify bars.

For regime detection, the model takes a vector of features from recent price action—things like normalized returns, volatility ratios, correlation with moving averages, and fractal dimensions—and outputs probabilities for three classes: trending, ranging, and volatile. The EA then selects which sub-strategy to run based on the highest probability.

This isn't a black box that spits out buy/sell signals. It's a meta-layer that decides how to trade. The actual entry and exit logic stays rule-based. That's key: you keep interpretability for the trading decisions while using ML for the regime classification problem, which is naturally suited to pattern recognition.

A common question I get: "Why not just use a neural network to generate buy/sell signals directly?" Because that's a harder problem—price direction prediction is noisy, non-stationary, and prone to overfitting. Regime classification is a simpler, more stable task. The market has only a few states at any time. Getting those states right gives you a massive edge without the headache of predicting exact price levels.

Let me be blunt: if you can't explain why your model classified a bar as trending, you're doing it wrong. The ONNX model should complement your understanding, not replace it. I always keep a debug panel in my EA that prints the raw feature values alongside the model output. That way, when something goes wrong, I can trace it back to the data, not the magic box.

Training the ONNX Model: What I Actually Do

You can't skip this part. The EA is only as good as the model it loads. Here's the pipeline I use, step by step. I'll be specific about numbers so you can replicate it exactly.

Step 1: Feature Engineering

I extract 12 features from each bar on H1 data. Why 12? Enough to capture the regime signature without overfitting. Here's the full list with exact formulas:

  • Normalized return over 5, 10, and 20 bars: (Close - Close[n]) / ATR(14). This scales returns by volatility, so a 2% move in a low-vol pair isn't treated the same as a 2% move in a high-vol pair.
  • Volatility ratio: ATR(10) / ATR(50). Values > 1.5 indicate expanding volatility, < 0.7 indicate compression. I cap this at [0.3, 3.0] to avoid outliers from flash crashes.
  • ADX(14) normalized to [0,1] by dividing by 100. Raw ADX is 0-100, but the model works better with bounded inputs.
  • Bollinger Band width (2 std dev) divided by middle band. This gives a percentage measure of volatility relative to price level.
  • Correlation between price and a 10-bar linear trendline (Pearson's r). Values near +1 or -1 indicate strong trending, near 0 indicate ranging.
  • Fractal dimension estimated from Higuchi's algorithm over 20 bars—helps distinguish trending from noisy ranging. Fractal dimension near 1.0 is a smooth trend, near 2.0 is random noise.
  • Volume ratio: current volume / average volume(20). Volume spikes often precede regime changes.

I collect these for every bar across EURUSD, GBPUSD, USDJPY, and AUDUSD on H1, from January 2020 to December 2023. That's about 100,000 samples per pair, 400k total. You could use more pairs, but I found four major pairs give enough variety without diluting the signal.

A practical tip: when you compute these features in Python, store them as a CSV with a timestamp column. Then you can easily align them with your labels and debug any mismatches. I once spent two days chasing a bug that turned out to be a one-bar offset between my feature DataFrame and my label array. Don't be that guy.

Step 2: Labeling the Data

You need ground truth labels. This is the trickiest part—bad labels mean a bad model no matter how good your features are. I use a multi-step heuristic that's been battle-tested:

  1. Compute a linear regression slope over 50 bars. If the slope's p-value < 0.05 and R² > 0.6, label that bar as trending. The p-value ensures statistical significance, R² ensures the trend explains most of the price movement.
  2. For remaining bars, compute the 20-bar range as a percentage of the 50-bar ATR. If range/ATR < 0.8, label as ranging. Tight range relative to volatility means the market is coiling.
  3. Everything else that has ATR(10) > 1.5 * ATR(50) gets labeled volatile. This catches news events, breakouts, and panic moves.
  4. Unlabeled bars (the in-between) are discarded—about 15% of the data. These are ambiguous states like a weak trend or a mild expansion. Better to train on clean examples than confuse the model with noise.

This gives you a clean, three-class dataset. No, it's not perfect, but it's consistent and reproducible. If you want to refine it, try adding a "transition" class for bars between regimes—I experimented with that but found it didn't improve performance enough to justify the complexity.

One gotcha: the labeling heuristic itself has biases. The linear regression method will miss very short trends (less than 50 bars). If you trade on M5, you'll need to adjust the lookback periods. For H1, 50 bars is about two trading days, which catches most meaningful moves.

Step 3: Train a Small Neural Network

I use PyTorch for training. Architecture: input layer (12 features) → dense(64, ReLU) → dropout(0.3) → dense(32, ReLU) → output(3, Softmax). Train for 50 epochs with cross-entropy loss and Adam optimizer. The model is intentionally small—under 10,000 parameters—so it runs fast in MQL5.

Key training details: batch size 64, learning rate 0.001 with a step decay every 15 epochs (multiply by 0.1). I use 80/20 train-validation split. Validation accuracy typically hits 82-85% on held-out data. That's good enough for regime detection—you don't need 99% accuracy because the sub-strategies have built-in robustness.

Export to ONNX with dynamic axes for batch size. The resulting file is about 180 KB. You can download a pre-trained version from my GitHub (link in the resources section at the end), but I strongly recommend training your own on the pairs and timeframes you actually trade. The model trained on EURUSD won't perform the same on XAUUSD or BTCUSD—different beasts entirely.

Here's the PyTorch export snippet I use:

import torch
import torch.nn as nn

class RegimeNet(nn.Module):
    def __init__(self, input_size=12):
        super().__init__()
        self.fc1 = nn.Linear(input_size, 64)
        self.dropout = nn.Dropout(0.3)
        self.fc2 = nn.Linear(64, 32)
        self.fc3 = nn.Linear(32, 3)
        
    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.dropout(x)
        x = torch.relu(self.fc2(x))
        return torch.softmax(self.fc3(x), dim=1)

model = RegimeNet()
# ... training code ...
dummy_input = torch.randn(1, 12)
torch.onnx.export(model, dummy_input, "regime_classifier.onnx",
                  input_names=['features'], output_names=['probabilities'],
                  dynamic_axes={'features': {0: 'batch_size'}})

Step 4: Validate Before Deployment

Don't just trust the validation accuracy. Run the model on out-of-sample data from 2024 and check the confusion matrix. I want to see:

MetricTargetMy ModelNotes
Trending precision≥ 80%84%Rarely false-flag a range as trend.
Ranging recall≥ 75%79%Catches most tight ranges.
Volatile F1≥ 0.700.73Hardest class due to rare events.

If the volatile class F1 is below 0.65, add more volatility examples to your training data—maybe include news event bars from ForexFactory. I've also found that oversampling the volatile class with synthetic data (SMOTE) helps, but be careful not to introduce artifacts.

Implementing the EA in MQL5

Now the fun part: getting that model into MetaTrader 5 and making it trade. Here's the core structure. I'll walk through every critical piece so you don't hit the common pitfalls.

Loading the ONNX Model

You need to place the .onnx file in Files/ or Indicators/ in your MetaTrader data folder. I use Files/ because it's cleaner. Then load it in OnInit():

#include <OnnxRuntime.mqh>

long modelHandle = INVALID_HANDLE;

int OnInit() {
    string modelPath = "Files\\regime_classifier.onnx";
    modelHandle = OnnxCreateFromBuffer(modelPath, ONNX_DEFAULT);
    if(modelHandle == INVALID_HANDLE) {
        Print("Failed to load ONNX model: ", GetLastError());
        return INIT_FAILED;
    }
    // Set input and output shapes
    ulong inputShape[] = {1, 12};
    ulong outputShape[] = {1, 3};
    OnnxSetInputShape(modelHandle, 0, inputShape);
    OnnxSetOutputShape(modelHandle, 0, outputShape);
    return INIT_SUCCEEDED;
}

A common mistake: forgetting to check the model path. If the file isn't found, OnnxCreateFromBuffer returns -1 and you get error 5200. Always verify the file exists in the correct folder. I add a debug print to list files in the directory during development.

Another trap: the ONNX runtime in MQL5 expects the model to be in a specific format. If you export with opset version 18 or higher, it might not load. I use opset 15 for maximum compatibility. Check your ONNX export settings—this one cost me an afternoon of head-scratching.

Running Inference Each Bar

On every new bar, you build the feature vector and run the model. Here's the complete inference function:

void RunRegimeInference() {
    float features[12];
    // Fill features array with your computed values
    features[

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles