Why ONNX in MetaTrader 5 Changes the Game for Algorithmic Traders
Most algorithmic traders I know have a love-hate relationship with machine learning. You spend weeks training a model in Python, get decent validation accuracy, then hit the wall: how do you get that model into MetaTrader in real-time? The old answer was socket connections, DLL bridges, or exporting predictions to a file and reading them in MQL — all brittle, latency-prone, and a pain to maintain.
That changed with MQL5 build 2360 and later. MetaQuotes added native support for the ONNX runtime (Open Neural Network Exchange). ONNX is an open format for representing machine learning models, supported by TensorFlow, PyTorch, scikit-learn, and most major frameworks. You train your model in Python, export it to ONNX format, and load it directly in an MQL5 indicator or Expert Advisor using the OnnxRuntime functions. No DLLs, no inter-process communication, no Python running alongside MT5.
This isn't theoretical. I've been running an ONNX-based indicator on EURUSD M15 for three months. It's not magic — you still need a decent model and solid risk management — but the integration is clean, the latency is sub-millisecond per tick, and it's all inside one MT5 terminal. Let me walk you through exactly how to build one.
What You'll Need Before Starting
This isn't a beginner MQL5 tutorial. You should know how to compile an indicator, use the OnCalculate() function, and understand indicator buffers. You also need some familiarity with Python for training the model. Here's the checklist:
- MetaTrader 5 build 2360 or newer — check Help > About in the terminal. Older builds don't have ONNX functions. I've seen traders on build 2340 spend hours debugging before realizing the issue.
- ONNX Runtime library — MT5 includes it internally. You don't install anything separately. The runtime version is typically 1.8 or later, which supports opset versions up to 15.
- A trained model in ONNX format — I'll show you a minimal example using a feedforward neural network trained on price features. You can also export from scikit-learn pipelines or XGBoost models.
- Python 3.8+ with
torchortensorflowandonnxpackages for exporting. If you're using PyTorch, installtorch-onnxfor smoother exports.
Step 1: Train and Export a Simple Price Prediction Model
Let's keep the model simple so the focus stays on the MQL5 integration. I'll train a small feedforward network that takes the last 20 normalized price returns and predicts the next 1-bar return direction (up or down). This is deliberately weak as a standalone predictor — you'd want more features in production — but it demonstrates the pipeline cleanly.
Here's the Python script to train and export:
import torch
import torch.nn as nn
import torch.onnx
import numpy as np
# Generate synthetic price data (replace with real OHLC)
np.random.seed(42)
n_samples = 10000
sequence_length = 20
X = np.random.randn(n_samples, sequence_length)
y = (np.random.rand(n_samples) > 0.5).astype(np.float32) # binary classification
# Define a simple feedforward network
class PricePredictor(nn.Module):
def __init__(self, input_size=20, hidden_size=32):
super().__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_size, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return self.sigmoid(x)
model = PricePredictor()
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Train for a few epochs
for epoch in range(5):
outputs = model(torch.FloatTensor(X))
loss = criterion(outputs.squeeze(), torch.FloatTensor(y))
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")
# Export to ONNX
dummy_input = torch.FloatTensor(np.random.randn(1, sequence_length))
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'}},
opset_version=11)
print("Model exported to price_predictor.onnx")
Copy the resulting price_predictor.onnx file into your MT5 Files folder (usually ...\MetaTrader 5\MQL5\Files\). The model expects a 1x20 vector of floats normalized to roughly zero mean and unit variance. In my actual setup, I use z-scores of the last 20 close prices as input.
Real-world training tip: If you're using real historical data instead of synthetic, make sure to normalize each batch independently. I once made the mistake of normalizing the entire dataset at once, then the model failed on live data because the mean and std dev shifted. Always normalize per-bar using a rolling window — exactly like we'll do in MQL5.
Step 2: Build the MQL5 Indicator Skeleton
Create a new custom indicator in MetaEditor (File > New > Custom Indicator). I'll call mine ONNX_PricePredictor. The key sections are:
- Input parameters — model file path, input sequence length, prediction threshold.
- Indicator buffers — one buffer for the raw prediction value (0 to 1), one for a binary signal (above/below threshold).
- ONNX handle — we'll store the model handle as a global variable.
- OnInit() — load the model, check input/output shapes.
- OnCalculate() — prepare input tensor, run inference, write to buffers.
Here's the complete code. I've added comments explaining each ONNX-specific step.
//+------------------------------------------------------------------+
//| ONNX_PricePredictor.mq5 |
//| (c) 2025, tradingbotmaker.com |
//+------------------------------------------------------------------+
#property copyright "tradingbotmaker.com"
#property link ""
#property version "1.00"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots 2
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_type2 DRAW_ARROW
#property indicator_color2 clrLimeGreen
#property indicator_width1 1
//--- input parameters
input string ModelFile = "price_predictor.onnx"; // ONNX model file
input int InputLength = 20; // Number of past prices
input double Threshold = 0.6; // Signal threshold (> = buy)
input int NormalizationPeriod = 100; // Lookback for z-score calc
//--- indicator buffers
double PredictBuffer[];
double SignalBuffer[];
//--- ONNX handle
long modelHandle = INVALID_HANDLE;
//--- price history for normalization
double priceHistory[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//--- set buffers
SetIndexBuffer(0, PredictBuffer, INDICATOR_DATA);
SetIndexBuffer(1, SignalBuffer, INDICATOR_DATA);
PlotIndexSetInteger(1, PLOT_ARROW, 159); // small circle
//--- load ONNX model
modelHandle = OnnxCreateFromFile(ModelFile, ONNX_DEFAULT);
if(modelHandle == INVALID_HANDLE)
{
Print("Failed to load ONNX model: ", ModelFile, ". Error: ", GetLastError());
return(INIT_FAILED);
}
//--- check input shape (we expect [batch_size, 20])
long inputCount = OnnxGetInputCount(modelHandle);
if(inputCount != 1)
{
Print("Expected 1 input, got ", inputCount);
return(INIT_FAILED);
}
//--- set input shape: we'll use batch_size = 1
long inputShape[] = {1, InputLength};
if(!OnnxSetInputShape(modelHandle, 0, inputShape))
{
Print("Failed to set input shape. Error: ", GetLastError());
return(INIT_FAILED);
}
//--- check output shape
long outputCount = OnnxGetOutputCount(modelHandle);
if(outputCount != 1)
{
Print("Expected 1 output, got ", outputCount);
return(INIT_FAILED);
}
long outputShape[];
if(!OnnxGetOutputShape(modelHandle, 0, outputShape))
{
Print("Failed to get output shape. Error: ", GetLastError());
return(INIT_FAILED);
}
Print("Model loaded. Output shape: ", outputShape[0], " x ", outputShape[1]);
//--- allocate price history
ArrayResize(priceHistory, NormalizationPeriod);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(modelHandle != INVALID_HANDLE)
{
OnnxRelease(modelHandle);
modelHandle = INVALID_HANDLE;
}
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(rates_total < NormalizationPeriod + InputLength)
return(0);
int start = prev_calculated > 0 ? prev_calculated - 1 : NormalizationPeriod + InputLength;
for(int i = start; i < rates_total; i++)
{
//--- collect last NormalizationPeriod closes for z-score
for(int j = 0; j < NormalizationPeriod; j++)
priceHistory[j] = close[i - NormalizationPeriod + 1 + j];
//--- compute mean and stddev
double mean = 0.0;
for(int j = 0; j < NormalizationPeriod; j++)
mean += priceHistory[j];
mean /= NormalizationPeriod;
double variance = 0.0;
for(int j = 0; j < NormalizationPeriod; j++)
variance += (priceHistory[j] - mean) * (priceHistory[j] - mean);
variance /= NormalizationPeriod;
double stddev = MathSqrt(variance);
if(stddev < 0.0001) stddev = 0.0001;
//--- build normalized input vector (last InputLength values)
float inputData[];
ArrayResize(inputData, InputLength);
for(int j = 0; j < InputLength; j++)
inputData[j] = (float)((close[i - InputLength + 1 + j] - mean) / stddev);
//--- run ONNX inference
float outputData[];
ArrayResize(outputData, 1);
if(!OnnxRun(modelHandle, ONNX_DEFAULT, inputData, outputData))
{
Print("ONNX inference failed at bar ", i, ". Error: ", GetLastError());
PredictBuffer[i] = 0.5;
SignalBuffer[i] = EMPTY_VALUE;
continue;
}
double prediction = (double)outputData[0];
PredictBuffer[i] = prediction;
//--- generate binary signal based on threshold
if(prediction > Threshold)
SignalBuffer[i] = low[i] - 10 * _Point; // arrow below bar
else if(prediction < (1.0 - Threshold))
SignalBuffer[i] = high[i] + 10 * _Point; // arrow above bar
else
SignalBuffer[i] = EMPTY_VALUE;
}
return(rates_total);
}
//+------------------------------------------------------------------+
Step 3: Understanding the ONNX Functions in MQL5
Let me highlight the key ONNX-specific calls because they're different from anything else in MQL5.
OnnxCreateFromFile()
This loads the .onnx file. It returns a handle you use for all subsequent operations. If the file isn't in the Files folder or the model version is incompatible, it returns INVALID_HANDLE. Always check this — I've seen traders skip the error check and wonder why the indicator draws nothing. The function also supports loading from a byte array via OnnxCreateFromBuffer(), but file loading is simpler for most use cases.
OnnxSetInputShape()
This tells the runtime what size input we'll pass. Our model expects [batch_size, 20]. I set batch_size to 1 since we process one bar at a time. You can set it larger if you batch multiple bars, but for real-time tick-by-tick, batch size 1 is fine. One gotcha: the shape must match exactly what the model was exported with. If you exported with dynamic axes (like we did), you can set any batch size. If you exported with fixed shapes, you're stuck with that size.
OnnxGetOutputShape()
This is optional but useful for debugging. I print the output dimensions in OnInit() to confirm the model returns a single scalar. If your model has multiple outputs, you'll need to handle each one separately.
OnnxRun()
This is the inference call. It takes the handle, a flags parameter (use ONNX_DEFAULT), the input array (must be a float[]), and an output array (also float[]). The arrays must be sized correctly beforehand. The function returns true on success. If it fails, check GetLastError() — common errors include shape mismatches or NaN inputs. I've also seen it fail silently when the model has unsupported operations (like custom layers).
Step 4: Compiling and Testing the Indicator
Compile the code in MetaEditor (F7). If you get errors, double-check your build version — the ONNX functions were added in build 2360. I've seen traders on older builds wonder why OnnxCreateFromFile is undefined. Also check that your .mq5 file is saved with UTF-8 encoding, otherwise some characters may cause compilation issues.
Attach the indicator to a chart. Go to the Navigator panel, drag ONNX_PricePredictor onto a EURUSD M15 chart. The indicator should show a blue line oscillating between 0 and 1 (the raw prediction) and green/red arrows for buy/sell signals.
If the line stays flat at 0.5, the model is running but returning neutral predictions. This usually means your input normalization doesn't match what the model was trained on. In my case, I trained on z-scores of returns, not raw prices. Adjust the normalization in OnCalculate() to match your training pipeline. Another common issue: if your model outputs logits instead of probabilities, you'll need to add a sigmoid activation in Python before exporting, or handle it in MQL5.
Input Parameters Reference
| Parameter | Type |
|---|






