You trained a PyTorch model in Python. Now it needs to live inside an MQL5 Expert Advisor. The bridge is ONNX — an interchange format that PyTorch exports natively and MetaTrader 5 loads natively. The conversion is one function call. Getting it to actually work in MT5 is where it gets interesting.

This article walks through the 2026 export workflow: the right opset, the right way to handle dynamic axes, the validation step nobody mentions, and the LSTM gotcha that costs people hours.

The minimal working export

A complete, working PyTorch-to-ONNX pipeline for an MT5-bound model:

export.py
import torch import torch.nn as nn class PricePredictor(nn.Module): def __init__(self, input_size=1, hidden=64): super().__init__() self.lstm = nn.LSTM(input_size, hidden, batch_first=True) self.fc = nn.Linear(hidden, 1) def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :]) model = PricePredictor() model.load_state_dict(torch.load("weights.pt")) model.eval() # MANDATORY before export dummy = torch.randn(1, 120, 1) # (batch, seq, features) torch.onnx.export( model, dummy, "eurusd.onnx", input_names=["input"], output_names=["output"], opset_version=17, # MT5-safe opset dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, do_constant_folding=True, )

This produces eurusd.onnx, ready to drop into MQL5\Files\ and load with OnnxCreate. Three things in this snippet are important and easy to miss:

The new exporter vs the legacy one

PyTorch ships two ONNX exporters in 2026:

Start with the legacy exporter. Only switch to dynamo=True if the legacy one fails on your specific model.

Dynamic axes — what to keep dynamic

When you declare dynamic_axes, you're saying "this dimension's size will be set at runtime, not export time." On the MQL5 side, you'll set it with OnnxSetInputShape.

Rule of thumb: only the batch dimension should be dynamic. Everything else — sequence length, feature count, hidden size — fix at export time. This is especially important for LSTMs (see below).

Validate before shipping to MT5

Don't drop a freshly-exported model into MQL5 and hope. Validate in Python first by running inference through onnxruntime and comparing to the original PyTorch output:

validate.py
import torch import numpy as np import onnxruntime as ort x = torch.randn(1, 120, 1) # PyTorch output y_torch = model(x).detach().numpy() # ONNX output sess = ort.InferenceSession("eurusd.onnx") y_onnx = sess.run(None, {"input": x.numpy()})[0] assert np.allclose(y_torch, y_onnx, atol=1e-5), "Outputs diverged!" print("OK — ONNX matches PyTorch within 1e-5")

If outputs match within a small tolerance (1e-5 is generous), the export is faithful. If they diverge, something went wrong — usually model.eval() wasn't called, or there's an op that didn't translate correctly. Don't proceed to MT5 until this passes.

Inspect with Netron

Open netron.app and drag your .onnx file in. You see the full computation graph. What to check:

The shape and name you see in Netron are what you pass to OnnxSetInputShape in MQL5. They must match exactly.

LSTM — the seq_len trap

If your model contains an LSTM and you declare seq_len as a dynamic axis, the export sometimes silently produces a broken model (different output from PyTorch). The full diagnosis and fix is in the PyTorch LSTM export guide.

Short version: keep seq_len static at export time. If you need to run inference on different sequence lengths, re-export the model for each one.

where to train

Training on GPU is dramatically faster.

If you're exporting models, you're training them. Cloud GPUs are hourly:

Affiliate links — see full comparison.