This is the bug that costs people the most time and is the hardest to spot. Symptoms: the ONNX model loads fine, runs without errors, produces numbers — but in the Strategy Tester the EA's predictions look like noise. The validation step you did in Python passed perfectly. There's nothing obviously wrong. It's almost always normalization mismatch.

What goes wrong

During Python training, you computed something like X_train_normalized = (X_train - mean) / std using statistics from the training set. The model learned to expect inputs in that normalized space (mean=0, std=1). When you ship to MQL5 and feed raw bars in, the model sees inputs that are nowhere near its training distribution. The output is garbage.

The frustrating thing: the ONNX runtime doesn't error. The model "works" — it produces numbers, just numbers that don't mean anything.

Save the normalization params at training

save_norm.py
import json, numpy as np # After computing X_train mean/std: norm_stats = { "mean": X_train.mean(axis=0).tolist(), "std": X_train.std(axis=0).tolist(), } with open("normalization.json", "w") as f: json.dump(norm_stats, f)

If your features have per-feature means/stds, save the full array. If you normalized scalar-wise (single mean and std for everything), save those two numbers.

Load and apply in MQL5 identically

load_norm.mq5
double NormMean[N_FEATURES]; double NormStd[N_FEATURES]; bool LoadNormalization() { int h = FileOpen("normalization.json", FILE_READ|FILE_TXT); if(h == INVALID_HANDLE) return false; string json = ""; while(!FileIsEnding(h)) json += FileReadString(h); FileClose(h); // Parse JSON, fill NormMean[] and NormStd[] // ... use any MQL5 JSON parser ... return true; } void Normalize(matrixf &m) { for(int i = 0; i < N_FEATURES; i++) m[0][i] = (float)(((double)m[0][i] - NormMean[i]) / NormStd[i]); }

The same operations Python did in (X - mean) / std — performed bit-identically on the MQL5 side. Order of subtraction and division matters because float arithmetic isn't perfectly associative.

How to diagnose a mismatch in 60 seconds

Add a debug print on first inference. Compare the input vector your MQL5 code is passing to OnnxRun with the input vector you'd get from your Python pipeline on the same bar:

  1. In MQL5, before the OnnxRun call, print the input matrix to the journal.
  2. In Python, load the same historical bar, run your normalization pipeline, print the same vector.
  3. The two should match to ~6 decimal places. If they're an order of magnitude off, it's the normalization.

If you skip this check and just look at trading P/L, you'll waste days assuming the model is bad when actually the input was wrong.

Alternative: bake normalization into the graph

Some exporters can include the normalization as part of the model itself — the MQL5 side feeds raw values and the graph does the scaling internally. With scikit-learn, wrap the model in a Pipeline with a StandardScaler — the sklearn export handles this automatically.

For PyTorch/TensorFlow, you can prepend a nn.BatchNorm1d (with fixed running stats from training) or a custom scaling layer. More work but eliminates the class of bug entirely.