Why ONNX in MQL5 Changes the Game for EA Development
You've probably heard the buzz about MetaTrader 5's ONNX integration. It's not just another indicator — it's a way to run machine learning models directly inside your EA without any external Python scripts, sockets, or DLL bridges. For anyone who's tried to marry ML predictions with live trading, you know the pain: latency, connection drops, version mismatches. ONNX kills all that.
I've been building EAs for over a decade, and I can tell you — most "AI trading" systems you see marketed are smoke and mirrors. They train a model on historical data, overfit it to death, then sell it as a black box. But the ONNX approach is different. You bring your own model, you control the features, and you see exactly what the EA does with each prediction. No magic. Just math.
In this post, I'll walk you through building a complete MQL5 ONNX EA from scratch. We'll cover loading a pre-trained ONNX model, preprocessing price data into features the model understands, executing trades based on predictions, and running a meaningful backtest. You'll need basic MQL5 knowledge and a trained ONNX model (I'll point you to resources for that). Let's get into the code.
What You Need Before Starting
This isn't a beginner tutorial. You should know your way around the MetaEditor, understand how to compile and test an EA, and have some familiarity with arrays and matrix operations in MQL5. Here's the full prerequisite list:
- MetaTrader 5 build 3500+ — ONNX support started in build 3300, but I recommend the latest stable build. Check Help > About.
- A trained ONNX model file — I'll assume you have a classification model that takes, say, 10 price-derived features and outputs a probability for "up" vs "down". You can train one in Python with scikit-learn and export via skl2onnx, or use a PyTorch model exported via torch.onnx. I'll include a dummy model for testing later.
- The ONNX Runtime DLL — MT5 ships with onnxruntime.dll in the root folder. If it's missing, reinstall or download from the official ONNX Runtime GitHub.
- Patience for debugging — ONNX inference in MQL5 is still new. You'll hit shape mismatch errors. It's part of the process.
Step 1: Loading the ONNX Model in MQL5
First, you create an long handle for the model using OnnxCreateFromBuffer. You need to embed the model file into your EA as a resource. Here's the boilerplate:
//+------------------------------------------------------------------+
//| Include the ONNX model as a resource |
//+------------------------------------------------------------------+
#resource "\\Files\\my_model.onnx" as uchar modelBuffer[]
//+------------------------------------------------------------------+
//| Global handle for the model |
//+------------------------------------------------------------------+
long modelHandle = INVALID_HANDLE;
//+------------------------------------------------------------------+
//| OnInit() – Load the model |
//+------------------------------------------------------------------+
int OnInit()
{
modelHandle = OnnxCreateFromBuffer(modelBuffer, ONNX_DEFAULT);
if(modelHandle == INVALID_HANDLE)
{
Print("Failed to create ONNX model. Error: ", GetLastError());
return INIT_FAILED;
}
// Set input and output shapes
ulong inputShape[] = {1, 10}; // batch size 1, 10 features
if(!OnnxSetInputShape(modelHandle, 0, inputShape))
{
Print("Failed to set input shape. Error: ", GetLastError());
return INIT_FAILED;
}
ulong outputShape[] = {1, 2}; // 2 outputs: prob(up), prob(down)
if(!OnnxSetOutputShape(modelHandle, 0, outputShape))
{
Print("Failed to set output shape. Error: ", GetLastError());
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}Notice the #resource directive — it embeds the ONNX file into the compiled EX5. That means your EA is self-contained. No external file dependencies at runtime. This is huge for portability. Just don't lose the source model file.
You must match the input and output shapes exactly to what your model expects. If your model takes 7 features, change 10 to 7. If it outputs a single value (like a regression), adjust accordingly. Mismatch here is the #1 cause of crashes — the ONNX runtime won't give you a friendly error, just a cryptic ERR_ONNX_INTERNAL.
Handling Multiple Inputs and Outputs
Some models have multiple input tensors — say, separate price and volume streams. In that case, you call OnnxSetInputShape for each input index. Same for outputs. The index starts at 0. If you're unsure about your model's structure, open it with Netron (free tool) to inspect the graph. I've wasted hours guessing shapes before discovering that tool.
Step 2: Preprocessing Market Data Into Features
Raw price data — open, high, low, close, volume — is rarely useful directly. Your model was trained on features like normalized returns, RSI, or spread. You need to replicate that exact preprocessing in MQL5. Here's a typical example for a model that uses 10 features:
//+------------------------------------------------------------------+
//| Build feature vector from recent price data |
//+------------------------------------------------------------------+
bool BuildFeatures(double &features[])
{
ArrayResize(features, 10);
// Feature 1: 5-period return
double close5 = iClose(_Symbol, PERIOD_CURRENT, 5);
double close0 = iClose(_Symbol, PERIOD_CURRENT, 0);
if(close5 == 0.0) return false;
features[0] = (close0 - close5) / close5;
// Feature 2: 10-period return
double close10 = iClose(_Symbol, PERIOD_CURRENT, 10);
if(close10 == 0.0) return false;
features[1] = (close0 - close10) / close10;
// Feature 3: RSI(14)
double rsiBuffer[];
ArraySetAsSeries(rsiBuffer, true);
int rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
if(rsiHandle == INVALID_HANDLE) return false;
if(CopyBuffer(rsiHandle, 0, 0, 1, rsiBuffer) < 1) return false;
features[2] = rsiBuffer[0] / 100.0; // normalize to [0,1]
// ... fill features 3-9 similarly with indicators or custom calculations
// Feature 10: recent volatility (ATR(14) / close)
double atrBuffer[];
ArraySetAsSeries(atrBuffer, true);
int atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(atrHandle == INVALID_HANDLE) return false;
if(CopyBuffer(atrHandle, 0, 0, 1, atrBuffer) < 1) return false;
features[9] = atrBuffer[0] / close0;
return true;
}Critical detail: your features must be normalized exactly as during training. If you trained with StandardScaler in Python, you need to store the mean and standard deviation for each feature and apply them here. Many traders skip this and wonder why the model outputs garbage. Don't be that trader.
I prefer to keep the normalization parameters as EA input parameters so I can tweak them without recompiling. Something like:
input double FeatureMean0 = 0.0012;
input double FeatureStd0 = 0.0150;
// ... etc for all 10 featuresThen in BuildFeatures, after calculating each raw feature, apply (raw - mean) / std.
Feature Engineering Checklist
Before you write a single line of OnTick logic, verify these three things:
- Feature order matches training — Your Python training script fed features in a specific sequence. Your MQL5 code must produce them in the exact same order. A single swapped column and your model is useless.
- Lookback period is consistent — If your model uses 20 bars of history, ensure your EA has at least that many bars loaded before making predictions. Check
Bars(_Symbol, _Period)in OnInit and refuse to trade if insufficient. - Handle missing indicator values — Some indicators like RSI return 0 or EMPTY_VALUE on the first bars. I add a
if(rsiBuffer[0] == 0.0) return false;check to skip those ticks.
Step 3: Running Inference and Getting Predictions
With the model loaded and features ready, inference is a single function call:
//+------------------------------------------------------------------+
//| Run ONNX inference and return predicted direction |
//+------------------------------------------------------------------+
int PredictDirection(double &features[])
{
matrix inputMatrix(1, 10);
for(int i = 0; i < 10; i++)
inputMatrix[0][i] = features[i];
matrix outputMatrix(1, 2);
if(!OnnxRun(modelHandle, ONNX_NO_CONVERSION, inputMatrix, outputMatrix))
{
Print("ONNX inference failed. Error: ", GetLastError());
return -1; // error
}
// outputMatrix[0][0] = prob(up), outputMatrix[0][1] = prob(down)
if(outputMatrix[0][0] > outputMatrix[0][1])
return 1; // buy
else
return -1; // sell
}The ONNX_NO_CONVERSION flag tells the runtime not to convert data types — we're using double matrix, which matches most models. If your model expects float, use ONNX_CONVERSION_INPUT_FLOAT.
I've seen people get confused about the matrix dimensions. Your model's input layer defines the shape. If you trained with batch dimension, you must include it. A single prediction is still a 1×N matrix. MQL5's matrix class makes this clean, but be careful with memory — creating matrices on every tick adds up. I reuse static matrices declared at global scope.
Performance Considerations
ONNX inference on a simple feedforward network (say, 3 hidden layers with 64 neurons each) takes about 0.3–0.8 milliseconds per call on a modern CPU. That's fine for most forex pairs. But if you're running on 28 symbols simultaneously, or your model has 10+ layers with 1000+ neurons each, you'll see delays. I've tested a CNN model that took 12ms per inference — that's 12% of a 100ms tick interval. Not acceptable for scalping.
If performance is a concern, limit your EA to run inference only on new bar opens rather than every tick. Add a static datetime variable to track the last bar time:
static datetime lastBarTime = 0;
if(Time[0] == lastBarTime) return;
lastBarTime = Time[0];This reduces inference frequency from hundreds per minute to once per minute on M1, or once per hour on H1.
Step 4: Executing Trades Based on Predictions
Now we wire the prediction into a simple trading logic. I'll show a minimal example — enter a position when prediction confidence exceeds a threshold, exit on opposite signal or stop loss:
//+------------------------------------------------------------------+
//| OnTick() – Main EA logic |
//+------------------------------------------------------------------+
void OnTick()
{
// Only trade on new bar
static datetime lastBar = 0;
if(Time[0] == lastBar) return;
lastBar = Time[0];
double features[];
if(!BuildFeatures(features)) return;
int signal = PredictDirection(features);
if(signal == -1) return; // inference error
double probUp = 0.0; // we'd extract this properly in real code
// In practice, get prob from outputMatrix after OnnxRun
// Only act if confidence > 60%
if(signal == 1 && probUp > 0.6 && PositionsTotal() == 0)
{
double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - 200 * _Point;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + 400 * _Point;
Trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, 0.1, SymbolInfoDouble(_Symbol, SYMBOL_ASK), sl, tp, "ONNX EA");
}
else if(signal == -1 && (1 - probUp) > 0.6 && PositionsTotal() == 0)
{
double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) + 200 * _Point;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) - 400 * _Point;
Trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, 0.1, SymbolInfoDouble(_Symbol, SYMBOL_BID), sl, tp, "ONNX EA");
}
}Notice I check PositionsTotal() == 0 — this EA runs one position at a time. For a real system, you'd add position management, partial closes, or pyramid rules. The confidence threshold (0.6 here) is a critical parameter to optimize. Too low, and you overtrade on noise. Too high, and you miss good setups.
One mistake I see repeatedly: traders forget to handle the case where the model returns equal probabilities. That's a coin flip — you should skip that bar. Also, never trade on the first bar after model load; the indicators used for features may not have enough history yet.
Money Management and Risk Settings
Hardcoding stop loss and take profit as fixed pips is fine for a demo, but for production you'll want more advanced controls. Here's a table of input parameters I typically add:
<td style="border:1px solid #c8d8e4;padding:7px 10px;font-size:12px;| Parameter | Type | Default | Description |
|---|---|---|---|
| ConfidenceThreshold | double | 0.60 | Minimum probability required to enter a trade. |






