What You'll Learn and Why It Matters
Running a trained machine learning model directly inside MetaTrader 5 used to require external DLLs, Python bridges, or REST API calls — all adding latency, complexity, and failure points. Since build 3400, MQL5 ships with a native ONNX Runtime integration. That means you can load an .onnx model file, feed it market data, and get predictions inside your EA or indicator with no external dependencies.
This guide walks you through the entire workflow: exporting or obtaining a model in ONNX format, loading it in MQL5, preparing input tensors, running inference, and using the output in a real trading script. You'll also learn the practical limits — what ONNX can and can't do inside the MT5 sandbox — and how to avoid the common pitfalls that trip up most developers on their first attempt.
I've been using this since build 3400 hit the beta channel, and I can tell you: the first time you get a prediction from a model you trained in Python, running inside an MT5 EA with zero external dependencies, it feels like magic. But the devil is in the tensor shapes and data types. I've seen people spend days debugging a model that loaded fine but gave garbage output — all because the input tensor was float64 instead of float32.
Prerequisites
Before you write a single line of MQL5 code, make sure your environment is ready:
- MetaTrader 5 build 3400 or newer — Help > About shows your build. If you're below 3400, run the update via Help > Check for Updates. ONNX support arrived in build 3400, and some later builds fixed tensor-handling bugs, so always run the latest. I've seen build 3450 fix a stubborn crash with
OnnxRunon certain model architectures, and build 3500 added better error messages for shape mismatches. - A compiled .onnx model file — You need a model exported from PyTorch, TensorFlow, scikit-learn (via ONNXMLTools), or any framework that supports ONNX export. The model must be in
opset_version15 or lower for best compatibility with MQL5's runtime. Opset 17+ models will fail withERR_ONNX_INTERNALon unsupported operators. I learned this the hard way when my PyTorch model exported at opset 17 produced error 5100 on every call. - An MQL5 development environment — MetaEditor (F4 inside MT5). You'll write your EA or indicator here.
- A demo account — ONNX code runs fine in the Strategy Tester, but you'll test live inference on a demo account first. Real-money testing of AI-driven logic without thorough backtesting is reckless. I run at least 2 weeks of demo forward-testing before even considering a live deployment.
I also recommend you have the Netron app handy — it visualizes ONNX model structure, which is invaluable when you're figuring out input/output tensor names and shapes. I keep Netron open in a browser tab next to MetaEditor whenever I'm debugging a new model. It shows you the exact data type, shape, and name of every tensor node.
Step-by-Step: Loading and Running an ONNX Model in MQL5
1. Place the .onnx File in the Correct Folder
MQL5 loads ONNX models from the Files folder of your terminal's data directory. The path is:
%APPDATA%\MetaQuotes\Terminal\[INSTANCE_ID]\MQL5\Files\
Or via the terminal: File > Open Data Folder > MQL5 > Files. Copy your model.onnx file there. Do not put it in Indicators or Experts — the OnnxRuntime functions use the sandboxed Files directory.
If you're testing in the Strategy Tester, the file must also be present in the tester's virtual file system. The simplest approach: place it in the global Files folder — the tester inherits it. But here's a gotcha: if you run multiple terminal instances, each has its own Files folder. I once spent an hour wondering why my model loaded on one MT5 instance but not another — wrong instance's data folder. Check the terminal path in Help > About to confirm which instance you're working with.
Another edge case: the Strategy Tester on a remote agent (e.g., in a VPS cluster) may not have access to your local Files folder. In that scenario, embedding the model as a resource (see step 2) is mandatory.
2. Define the ONNX Handle in Your Script
You declare a handle of type long and then call OnnxCreateFromBuffer() or OnnxCreate(). I prefer OnnxCreateFromBuffer() because it lets you embed the model as a resource, avoiding file-path issues in the tester. Here's the basic setup:
#resource "\\Files\\model.onnx" as uchar modelBuffer[]
long onnxHandle = OnnxCreateFromBuffer(modelBuffer, ONNX_DEFAULT);
if(onnxHandle == INVALID_HANDLE)
{
Print("Failed to create ONNX runtime. Error: ", GetLastError());
return;
}
If you use OnnxCreate() with a file path, the path is relative to the Files folder, e.g., OnnxCreate("model.onnx", ONNX_DEFAULT). I've had path issues with OnnxCreate() in the Strategy Tester — it sometimes returns INVALID_HANDLE with error 5005 (file not found) even when the file exists. The buffer approach eliminates that entirely. The #resource directive compiles the model into your EX5 file, so you only need to distribute one file.
3. Set Up Input and Output Tensors
ONNX models expect tensors — multi-dimensional arrays with specific shapes and data types. You must tell MQL5 what those shapes are. Use OnnxSetInputShape() and OnnxSetOutputShape(). The tensor indices start at 0.
For example, a simple price-prediction model might expect a 2D float tensor of shape [1, 10] — one sample of 10 features:
long inputShape[] = {1, 10};
long outputShape[] = {1, 1};
if(!OnnxSetInputShape(onnxHandle, 0, inputShape))
{
Print("Failed to set input shape. Error: ", GetLastError());
OnnxRelease(onnxHandle);
return;
}
if(!OnnxSetOutputShape(onnxHandle, 0, outputShape))
{
Print("Failed to set output shape. Error: ", GetLastError());
OnnxRelease(onnxHandle);
return;
}
Critical: The shape dimensions must exactly match what your model expects. Use Netron to inspect your model's input node — it shows the name, data type, and shape. If your model expects float32[None, 10], you set {1, 10} for a batch size of 1. Dynamic batch sizes are supported: you can pass a shape with -1 for the batch dimension, but MQL5's runtime handles that internally. I've found that explicitly setting the batch dimension to 1 is more reliable than relying on dynamic shapes — dynamic shapes can cause unpredictable memory allocation.
If your model has multiple inputs or outputs (e.g., an LSTM with both price and volume inputs), you need to call OnnxSetInputShape() for each input index. Check OnnxGetInputCount() to verify the count matches your Netron inspection.
4. Prepare Input Data and Run Inference
Now you fill a vector or array with your features — typically normalized price data, indicators, or whatever your model was trained on. Then call OnnxRun():
float inputData[] = {0.5f, 0.3f, -0.1f, 0.8f, 0.2f, -0.4f, 0.6f, 0.1f, -0.3f, 0.7f};
float outputData[];
ArrayResize(outputData, 1);
if(!OnnxRun(onnxHandle, ONNX_DEFAULT, inputData, outputData))
{
Print("ONNX inference failed. Error: ", GetLastError());
OnnxRelease(onnxHandle);
return;
}
Print("Model prediction: ", outputData[0]);
The ONNX_DEFAULT flag uses the default execution provider (CPU). MQL5 does not support GPU execution providers — all inference runs on the CPU. For a typical small model (under 1 MB), inference takes under 10 milliseconds, which is fine for real-time use on every tick. Larger models (10+ MB) can cause stuttering; test carefully. I once tried a 15 MB LSTM model and the terminal froze for 200 ms on each inference — completely unusable on a 1-minute chart. Stick to models under 2 MB for tick-by-tick use.
The input array must be of type float (32-bit). If you pass a double array, OnnxRun() will return error 5101 (type mismatch). I've tripped over this more than once when copying data directly from price arrays, which are double in MQL5.
5. Integrate into an Indicator or EA
In an indicator, you typically load the model once in OnInit() and run inference inside OnCalculate(). In an EA, load in OnInit() and run in OnTick(). Always release the handle in OnDeinit():
void OnDeinit(const int reason)
{
if(onnxHandle != INVALID_HANDLE)
OnnxRelease(onnxHandle);
}
Here's a minimal indicator skeleton that calls an ONNX model on each new bar:
#property indicator_chart_window
#resource "\\Files\\model.onnx" as uchar modelBuffer[]
long onnxHandle;
int OnInit()
{
onnxHandle = OnnxCreateFromBuffer(modelBuffer, ONNX_DEFAULT);
if(onnxHandle == INVALID_HANDLE)
return INIT_FAILED;
long inputShape[] = {1, 10};
long outputShape[] = {1, 1};
OnnxSetInputShape(onnxHandle, 0, inputShape);
OnnxSetOutputShape(onnxHandle, 0, outputShape);
return INIT_SUCCEEDED;
}
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(prev_calculated == rates_total)
return rates_total;
// Build feature vector from last 10 closes
float inputData[10];
for(int i = 0; i < 10; i++)
inputData[i] = (float)close[rates_total - 1 - i];
float outputData[1];
if(OnnxRun(onnxHandle, ONNX_DEFAULT, inputData, outputData))
Comment("ONNX prediction: ", outputData[0]);
return rates_total;
}
void OnDeinit(const int reason)
{
if(onnxHandle != INVALID_HANDLE)
OnnxRelease(onnxHandle);
}
Notice I'm using raw close prices here — that's fine for a demo, but in a real model you'd normalize them first. Also note the prev_calculated check: it prevents running inference on every tick when the bar hasn't closed. For tick-level models, remove that check and run in OnTick() instead.
Exporting a Model from Python to ONNX
Most MQL5 developers train models in Python. Here's how to export a simple scikit-learn model to ONNX that works with MQL5:
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
# Train a tiny model (10 features, 100 trees)
model = RandomForestRegressor(n_estimators=50, max_depth=5)
X_train = np.random.randn(1000, 10).astype(np.float32)
y_train = np.random.randn(1000)
model.fit(X_train, y_train)
# Convert to ONNX with opset 15
initial_type = [('float_input', FloatTensorType([None, 10]))]
onx = convert_sklearn(model, initial_types=initial_type, target_opset=15)
# Save
with open("model.onnx", "wb") as f:
f.write(onx.SerializeToString())
print("Model saved. Input shape: [batch_size, 10], output: [batch_size, 1]")
Key points: use np.float32 (not float64), set target_opset=15, and keep the model small. Random forests blow up in size quickly — 50 trees with depth 5 gives you about 1 MB. For a neural network, use PyTorch's torch.onnx.export() with opset_version=15. Here's a PyTorch example:
import torch
import torch.nn as nn
class SimpleModel(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(10, 1)
def forward(self, x):
return self.fc(x)
model = SimpleModel()
dummy_input = torch.randn(1, 10)
torch.onnx.export(model, dummy_input, "model.onnx",
input_names=['float_input'],
output_names=['output'],
opset_version=15)
After export, always verify with Netron. I've caught cases where the output tensor name was auto-generated as something like onnx::MatMul_0 — you can rename it in the export call to make debugging easier.
Tips and Best Practices from Experience
- Normalize inputs exactly as you did during training. If your model was trained on z-score normalized data, apply the same mean and standard deviation inside MQL5 before feeding the tensor. A common mistake is feeding raw prices into a model trained on normalized features — the output will be garbage. I store the normalization parameters as
inputparameters in my EA so I can adjust them without recompiling. For example:input double NormMean = 1.2345;andinput double NormStd = 0.5678;. - Keep models small. MQL5's ONNX runtime runs on a single CPU thread. Models larger than 5 MB can cause noticeable delays on every tick. I aim for under 1 MB. Use quantization (e.g., ONNX Runtime's dynamic quantization to INT8) to shrink models without catastrophic accuracy loss. A quantized 800 KB model often performs nearly as well as a 4 MB float32 version. You can quantize in Python using
onnxruntime.quantization.quantize_dynamic(). - Test in the Strategy Tester first. The tester runs ONNX inference identically to live trading. Use the "Every tick" mode to simulate real-time conditions. Watch for
ERR_ONNX_INTERNAL(error 5100) — that usually means a shape mismatch or unsupported operator. I run at least 10,000 ticks in the tester before going live. Also test with "Open prices only" mode to verify the model behaves consistently across different data frequencies. - Use
OnnxGetInputCount()andOnnxGetOutputCount()to dynamically inspect your model. This is helpful when you're unsure about the number of inputs/outputs






