A zero-shot model (IBM Granite TTM-R2) in MT5 via ONNX: export pitfall, 0.86 ms per forecast, what it can forecast
You are missing trading opportunities:
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
Registration
Log in
You agree to website policy and terms of use
If you do not have an account, please register
I wanted to know whether one of the new pre-trained time-series models can run inside MetaTrader 5 itself, without Python, DLLs or WebRequest, so that it also works in the Strategy Tester. It can. These are my notes on IBM's Granite TTM-R2 (Tiny Time Mixers, Apache-2.0), with the numbers I measured.
The model. I used the 512-96 variant of ibm-granite/granite-timeseries-ttm-r2: 512 past values in, 96 forecast steps out, 805,280 parameters. Exported to ONNX (opset 17) it is 3.3 MB, so it fits in an .ex5 as a resource.
Export pitfall. The legacy exporter, torch.onnx.export with dynamo=False, mistranslates Tensor.unfold(dimension=-2) inside the patching step. It keeps the negative axis in Unsqueeze/Concat after a dimension has been added, so onnxruntime refuses the file with "[ShapeInferenceError] Incompatible dimensions for matrix multiplication" in the first patch-mixer MatMul. TTM's patches do not overlap (stride = patch length = 64), so for the export I replaced the patching with reshape + permute. The weights are unchanged and the PyTorch outputs are identical before and after.
MQL5 side. The float arrays can be passed straight to OnnxRun; the shapes just have to be set once after loading:
// Granite TTM-R2 (512-96): 512 past values in, 96 forecast steps out, float32. #resource "ttm_r2_512_96.onnx" as uchar TtmModel[] long g_ttm = INVALID_HANDLE; float g_in[], g_out[]; bool TtmOpen() { g_ttm = OnnxCreateFromBuffer(TtmModel, ONNX_DEFAULT); if(g_ttm == INVALID_HANDLE) { PrintFormat("OnnxCreateFromBuffer failed, err=%d", GetLastError()); return false; } const ulong in_shape[] = {1, 512, 1}; const ulong out_shape[] = {1, 96, 1}; if(!OnnxSetInputShape(g_ttm, 0, in_shape) || !OnnxSetOutputShape(g_ttm, 0, out_shape)) { PrintFormat("OnnxSetInputShape/OnnxSetOutputShape failed, err=%d", GetLastError()); OnnxRelease(g_ttm); g_ttm = INVALID_HANDLE; return false; } ArrayResize(g_in, 512); ArrayResize(g_out, 96); return true; } // Per-hour high-low range forecast for the next 96 hours from the last 512 closed H1 bars. // The model expects a standardised series: standardise on the way in, undo it on the way out. bool TtmRangeForecast(const string sym, double &range[]) { MqlRates r[]; ArraySetAsSeries(r, false); if(g_ttm == INVALID_HANDLE || CopyRates(sym, PERIOD_H1, 1, 512, r) != 512) return false; double x[512], mu = 0.0, var = 0.0; for(int i = 0; i < 512; i++) { double rng = (r[i].low > 0.0) ? MathLog(r[i].high / r[i].low) : 0.0; x[i] = MathLog(MathMax(rng, 1e-5)); // floor for flat bars; better: a low quantile of your own data mu += x[i]; } mu /= 512; for(int i = 0; i < 512; i++) var += (x[i] - mu) * (x[i] - mu); double sd = MathSqrt(var / 512) + 1e-6; for(int i = 0; i < 512; i++) g_in[i] = (float)((x[i] - mu) / sd); if(!OnnxRun(g_ttm, ONNX_DEFAULT, g_in, g_out)) { PrintFormat("OnnxRun failed, err=%d", GetLastError()); return false; } ArrayResize(range, 96); for(int k = 0; k < 96; k++) range[k] = MathExp((double)g_out[k] * sd + mu); // log(high/low) of hour k + 1 return true; }Does MT5 give the same numbers as Python? Not bit for bit, but close: the largest difference against PyTorch was 4.8e-7 on the standardised scale over three test series (the tester ships onnxruntime 1.25.1, Python had 1.31.0). Repeated runs inside MT5 return exactly the same values.
Speed. In the Strategy Tester one forecast takes 0.86 ms (median of 200 runs, CPU), and loading the model about 0.13 s. Ten years of H1 bars is about one minute of inference in total. Once per closed bar is fine; once per tick is not.
Input. The model expects each series to be standardised, and the 512-96 variant is meant for minute to hourly data (daily and weekly data have their own r2.1 variants). I feed it the log of each H1 bar's high-low range, standardised over the 512-bar window, and undo the standardisation on the output. A small floor avoids log(0) on flat bars; a low quantile of your own data works better than a fixed number.
What it can forecast. On 7 symbols (H1, 2015 to 2026) I compared its range forecasts with ATR and with two stronger simple baselines, a HAR model and an hour-of-day × level model. The error is the mean squared error of the log high-low range after calibration, shown relative to ATR. The chart is the part after the model's pre-training data, 2025–2026, 4 hours ahead:
Two smaller things. Calling ExpertRemove() inside OnInit makes the tester log "OnInit failed"; stop on the first OnTick instead. And Apache-2.0 asks you to give users a copy of the license, so if you distribute an EA with the model inside, plan how to show or write out that text.
If you have tried other foundation models (Chronos, TimesFM, Moirai) inside MT5, I would like to hear how they compared.