Why ONNX in MT5 Actually Matters
I've been writing MQL code for over a decade, and I've seen every "AI trading" fad come and go. Most of them involve piping data out of MetaTrader to some Python script, running predictions, and piping signals back. It's fragile, it lags, and it breaks when your broker changes the terminal build.
Then MetaQuotes dropped ONNX support in build 5000+ (around late 2024), and I was skeptical. Another half-baked feature? No. ONNX — Open Neural Network Exchange — is an open format for machine learning models. You train a model in Python (PyTorch, TensorFlow, scikit-learn), export it to a .onnx file, and load it directly inside an MQL5 Expert Advisor. No sockets, no DLLs, no Python bridge. The model runs natively in the MT5 terminal, on every tick, with sub-millisecond inference.
This isn't theoretical. I've built a working price-direction predictor using a simple feedforward neural network, exported it to ONNX, and called it from an EA. In this post, I'll walk you through the entire pipeline — from training data to a live-running EA — with honest notes on what works and what doesn't.
What You'll Need Before Starting
- MetaTrader 5 build 5000 or higher — check Help > About in the terminal. Older builds won't have the
OnnxRuntimefunctions. - Python 3.8+ with
torch(ortensorflow),onnx,numpy, andpandasinstalled. I use PyTorch because its ONNX export path is cleaner. - Historical price data — I export M1 data from MT5 via
CopyRates()to a CSV, then train on it. You could also use free sources like HistData.com. - Basic understanding of neural networks — you don't need to be a PhD, but you should know what a dense layer and ReLU activation are.
The Architecture: What the Model Actually Predicts
Let's be clear: nobody has a crystal ball. The model I built predicts the direction of price change over the next N bars — specifically, whether the close N bars from now will be higher or lower than the current close. This is a binary classification problem. The input features are simple technical indicators computed on the last 20 bars:
- Normalized close price (z-score over the window)
- RSI (14 period)
- ATR ratio (current ATR divided by 20-bar average ATR)
- Price change over 1, 3, and 5 bars
- Volume change ratio
I deliberately kept the feature set small to avoid overfitting on noise. More features don't mean better predictions — they mean more parameters to tune and more ways to fool yourself in backtests.
Step 1: Train and Export the ONNX Model (Python Side)
Here's the Python code I used to train a simple three-layer network. I'm showing the core parts — you'll need to adapt paths and data loading to your setup.
import torch
import torch.nn as nn
import torch.onnx
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
# Load your historical data (assumes CSV with columns: close, rsi, atr, etc.)
df = pd.read_csv('eurusd_m1_features.csv')
features = df[['close_z', 'rsi_14', 'atr_ratio', 'chg_1', 'chg_3', 'chg_5', 'vol_ratio']].values
labels = df['target'].values # 1 if close[N] > close[0], else 0
# Normalize
scaler = StandardScaler()
features = scaler.fit_transform(features)
# Convert to tensors
X = torch.tensor(features, dtype=torch.float32)
y = torch.tensor(labels, dtype=torch.float32).unsqueeze(1)
# Define a simple feedforward network
class PricePredictor(nn.Module):
def __init__(self, input_size=7):
super().__init__()
self.fc1 = nn.Linear(input_size, 32)
self.fc2 = nn.Linear(32, 16)
self.fc3 = nn.Linear(16, 1)
self.relu = nn.ReLU()
self.sigmoid = nn.Sigmoid()
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.sigmoid(self.fc3(x))
return x
model = PricePredictor()
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Train for 50 epochs (adjust based on your data size)
for epoch in range(50):
outputs = model(X)
loss = criterion(outputs, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f'Epoch {epoch}, Loss: {loss.item():.4f}')
# Export to ONNX
dummy_input = torch.randn(1, 7)
torch.onnx.export(
model,
dummy_input,
'price_predictor.onnx',
input_names=['input'],
output_names=['output'],
dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}
)
print("ONNX model saved as price_predictor.onnx")
Key detail: the dynamic_axes parameter is critical. It tells ONNX that the batch dimension can vary, so your MQL5 EA can pass a single row (one prediction) or a batch (multiple predictions) without re-exporting. I learned this the hard way after my first model only accepted fixed-size batches.
Where the StandardScaler Fits In
Your model was trained on normalized data, so every input the EA sends must be normalized using the exact same mean and standard deviation from training. You need to save those values. Add this to your Python script:
import json
scaler_params = {
'mean': scaler.mean_.tolist(),
'scale': scaler.scale_.tolist()
}
with open('scaler_params.json', 'w') as f:
json.dump(scaler_params, f)
Step 2: Build the MQL5 EA That Loads the ONNX Model
Now for the MQL5 side. You'll need to place price_predictor.onnx and scaler_params.json in the Files folder of your MT5 data directory (File > Open Data Folder > Files).
Here's the skeleton of the EA. I'm focusing on the ONNX integration — you'll need to add your own entry/exit logic, money management, and order handling.
//+------------------------------------------------------------------+
//| ONNX_PricePredictor.mq5 |
//| Your Name |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property version "1.00"
#property strict
#include
#include
// ONNX handle
long onnxHandle = INVALID_HANDLE;
long onnxRuntime = INVALID_HANDLE;
// Scaler parameters (loaded from JSON)
double scalerMean[];
double scalerScale[];
// Input features array
float inputData[7];
float outputData[1];
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Load the ONNX model
onnxHandle = OnnxCreateFromBuffer(
GetModelBuffer("price_predictor.onnx"),
ONNX_DEFAULT
);
if(onnxHandle == INVALID_HANDLE)
{
Print("Failed to create ONNX model. Error: ", GetLastError());
return(INIT_FAILED);
}
// Set input and output shapes
long inputShape[] = {1, 7}; // batch_size=1, 7 features
long outputShape[] = {1, 1};
OnnxSetInputShape(onnxHandle, 0, inputShape);
OnnxSetOutputShape(onnxHandle, 0, outputShape);
// Load scaler parameters
if(!LoadScalerParams())
{
Print("Failed to load scaler parameters");
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(IsNewBar()) // Only process on new bar to avoid over-trading
{
// 1. Compute features from current data
if(!ComputeFeatures(inputData))
return;
// 2. Normalize features using scaler
NormalizeFeatures(inputData);
// 3. Run inference
if(!OnnxRun(onnxHandle, ONNX_NO_CONVERSION, inputData, outputData))
{
Print("ONNX inference failed. Error: ", GetLastError());
return;
}
// 4. outputData[0] contains probability (0 to 1)
double prediction = (double)outputData[0];
double threshold = 0.6; // Adjust based on your validation
if(prediction > threshold)
// Signal: predict price up — place buy order
OpenBuy();
else if(prediction < (1.0 - threshold))
// Signal: predict price down — place sell order
OpenSell();
}
}
//+------------------------------------------------------------------+
//| Helper: load model as binary buffer |
//+------------------------------------------------------------------+
uchar GetModelBuffer(const string filename)
{
uchar buffer[];
int handle = FileOpen(filename, FILE_READ | FILE_BIN);
if(handle == INVALID_HANDLE)
return buffer;
FileReadArray(handle, buffer);
FileClose(handle);
return buffer;
}
//+------------------------------------------------------------------+
//| Helper: load scaler params from JSON |
//+------------------------------------------------------------------+
bool LoadScalerParams()
{
int handle = FileOpen("scaler_params.json", FILE_READ | FILE_TXT);
if(handle == INVALID_HANDLE)
return false;
string text;
while(!FileIsEnding(handle))
text += FileReadString(handle);
FileClose(handle);
// Simple JSON parsing (use a proper library for production)
// This extracts arrays from the JSON string
// For brevity, I'm showing the concept — implement your parsing
return true;
}
The Feature Computation Function
This is where you compute the same 7 features your Python model used. The exact calculation must match — even a rounding difference can wreck predictions. Here's a partial example:
bool ComputeFeatures(float &features[])
{
MqlRates rates[];
ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, _Period, 0, 21, rates) < 21)
return false;
// Feature 0: Close z-score over 20 bars
double closeMean = 0, closeStd = 0;
for(int i = 0; i < 20; i++)
closeMean += rates[i].close;
closeMean /= 20;
for(int i = 0; i < 20; i++)
closeStd += MathPow(rates[i].close - closeMean, 2);
closeStd = MathSqrt(closeStd / 20);
features[0] = (float)((rates[0].close - closeMean) / (closeStd > 0 ? closeStd : 1));
// Feature 1: RSI (14) — use your own RSI function
features[1] = (float)iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 0);
// ... continue for all 7 features
return true;
}
Step 3: Running in the Strategy Tester
Testing an ONNX-based EA in the Strategy Tester requires a few extra steps:
- Make sure
price_predictor.onnxandscaler_params.jsonare in theFilesfolder of the tester's data directory (usually a subfolder under\Tester\Agent-127.0.0.1\...\Files). You can place them in your mainFilesfolder and the tester copies them automatically in newer builds. - Enable "Allow External Expert Advisors" in the tester settings — ONNX runtime counts as an external library for security purposes.
- Use "Every tick" mode. ONNX inference is fast enough to run on every tick, but if you use "Control points" or "Open prices only", your model will miss intra-bar movement.
One gotcha I hit: the ONNX runtime in the tester uses a different working directory than live trading. If your model fails to load during backtest, check the Journal tab for the exact error — it's usually a file path issue. I ended up hardcoding the full path using TerminalInfoString(TERMINAL_DATA_PATH) and appending "\\Files\\".
Pros, Cons, and Honest Risks
Let's talk straight. ONNX in MT5 is powerful, but it's not a magic profit button.
What Works Well
- Speed: Inference takes under 0.1 milliseconds on a modern CPU. You can run a complex model on every tick without slowing down the terminal.
- No external dependencies: Once the model is loaded, the EA is fully self-contained. No Python process to crash, no REST API to timeout.
- Portability: You can share the EA and the ONNX file with someone else, and it just works — as long as they have MT5 build 5000+.
The Hard Truths
- Training is still separate: You cannot train a model inside MT5. You must do it in Python (or another framework) and export. This means your EA cannot adapt to changing market conditions without manual re-export.
- Overfitting is brutal: My first model had 92% accuracy on training data and 51% on out-of-sample data. The market is noisy, and neural networks are excellent at memorizing noise. You need rigorous walk-forward validation.
- Feature engineering matters more than architecture: I spent weeks tweaking the neural network layers and got nowhere. The breakthrough came when I added the ATR ratio feature. The model's performance jumped from 53% to 58% directional accuracy. Garbage in, garbage out — same as any other strategy.
- No GPU support: ONNX in MT5 runs on CPU only. If you're used to training on a GPU, the inference speed is still fine, but don't expect to run massive transformers.
Example Walkthrough: EURUSD M1 with 58% Accuracy
I trained the model on 6 months of EURUSD M1 data (Jan-Jun 2024), validated on July 2024, and tested on August 2024. Here are the results from the Strategy Tester with a simple fixed-lot strategy (0.1 lot, no stop loss — for evaluation only):
| Metric | Training (Jan-Jun) | Validation (Jul) |
|---|






