MQL5 ONNX Trend Filter EA: Cut False Signals in Half

Build an MQL5 ONNX trend filter EA to slash false signals. Train a neural network in Python, export to ONNX, and integrate with MetaTrader 5 for adaptive.

mql5-onnx-trend-filter-ea-cut-false-signals-in

Why Your EA Needs a Smarter Trend Filter

If you've been coding Expert Advisors for a while, you've seen the pattern: a strategy that looks great in a backtest falls apart in forward trading because it keeps entering during choppy, trendless markets. Most trend filters — moving averages, ADX, even Ichimoku — are reactive. They tell you what already happened. By the time they confirm a trend, half the move is gone, or they flip too late and you get whipsawed. I've watched traders chase the perfect SMA combination for years, only to realize that no single static period works across different market regimes.

That's where ONNX comes in. MetaTrader 5's ONNX Runtime support (available since build 5120, which rolled out in late 2023) lets you load a trained machine learning model directly into your EA. Instead of hardcoding a rule like "price above 200 SMA", you feed the model raw market data and get a probability score: how likely is this bar part of a strong trend? This isn't a magic bullet — it's a practical tool that, used right, can filter out the noise that kills your equity curve. You'll still have losing trades. The goal is to reduce the frequency of those entries that happen during range-bound markets, where even a good breakout strategy gets chopped up.

In this post, I'll walk you through building a practical MQL5 ONNX trend filter EA. We'll train a simple neural network offline, export it to ONNX format, integrate it into an EA, and test it on real data. I'll also show you the ugly parts — because honestly, there are several. If you're expecting a "set and forget" solution, stop reading now. This requires work, but the payoff is a filter that adapts to market conditions better than any static indicator. You'll need some Python skills and a willingness to get your hands dirty with data preprocessing.

What Makes a Good Trend Filter for ONNX?

Before we touch any code, let's talk about what we're actually trying to solve. A trend filter's job is binary: is the current market state trending (1) or not (0)? For an EA that trades breakouts or momentum entries, false signals usually happen when price is oscillating in a range. The filter should keep the EA flat during those periods. Simple enough in theory, but in practice, defining "trending" is where most filters fall short.

Traditional filters use lagging indicators. Your ONNX model can use multiple features simultaneously — price changes, volatility, volume, even time-based features — and learn non-linear relationships that no single indicator captures. The trade-off? You need to train it on historical data, and it's only as good as your training set. Garbage in, garbage out applies here more than anywhere else in trading. If your training data doesn't reflect the market conditions you'll trade live, the model will fail.

For this project, I trained a small feedforward neural network on EURUSD H1 data from 2020-2023. Features included:

  • Rate of change over 5, 10, and 20 bars — captures short, medium, and longer-term momentum
  • ATR(14) normalized by price — volatility measure that works across different price levels
  • Distance from 50-period SMA (as percentage) — how far price has deviated from its mean
  • Volume ratio (current volume / 20-bar average volume) — detects unusual activity
  • Bar-to-bar price change — captures immediate price action

The target label was "1" if the next 20 bars had a net move of at least 2x the current ATR in one direction, and "0" otherwise. That's a crude but functional definition of a trend that matters for a swing trader. I'll admit — I spent a weekend tweaking this definition. Too tight a threshold (1x ATR) and the model flagged everything as trending. Too loose (3x ATR) and it missed half the decent moves. The 2x ATR mark felt right for H1 swing trading on EURUSD. You'll need to experiment with your own symbol and timeframe — what works for EURUSD might fail on GBPJPY or gold.

Feature Engineering Pitfalls

The features I listed above aren't sacred. You might find that adding RSI or MACD histogram values improves performance on certain pairs. The key is to avoid overfitting. I tested about 20 different feature combinations before settling on these seven. Each additional feature increased the risk of the model memorizing noise rather than learning genuine patterns. A good rule of thumb: start with 5-7 features and only add more if you see clear improvement on out-of-sample data.

One thing that surprised me: including the hour of day as a cyclic feature (sin/cos encoding) actually helped on EURUSD, because trends tend to start during London and New York sessions. But I removed it for this version to keep the model simpler and more portable across symbols. You can experiment with session-based features if you're focused on a single pair.

Training and Exporting the Model

I won't pretend you can train a world-class model inside MetaTrader — you can't. You'll need Python (or your ML framework of choice). I used PyTorch, but TensorFlow/Keras works too. The key is exporting to the ONNX format that MT5 can load. The export process is straightforward, but getting the input/output shapes right requires attention.

Here's the skeleton of the training script in Python. I've stripped out the data loading and training loop for brevity, but the export part is what matters:

import torch
import torch.nn as nn
import torch.onnx
import numpy as np

class TrendFilterNet(nn.Module):
    def __init__(self, input_size=7):
        super().__init__()
        self.fc1 = nn.Linear(input_size, 16)
        self.fc2 = nn.Linear(16, 8)
        self.fc3 = nn.Linear(8, 1)
        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.sigmoid(self.fc3(x))
        return x

model = TrendFilterNet()
# ... training loop with your data (omitted for brevity) ...
model.eval()

dummy_input = torch.randn(1, 7)
torch.onnx.export(model, dummy_input, "trend_filter.onnx",
                  input_names=['features'],
                  output_names=['trend_prob'],
                  dynamic_axes={'features': {0: 'batch_size'},
                                'trend_prob': {0: 'batch_size'}})
print("Model exported to trend_filter.onnx")

Notice I used dynamic batching. That lets you pass one bar's features at a time (batch size 1) or multiple. For an EA, you'll typically call it bar by bar on each new tick. The dynamic_axes parameter is crucial — without it, MT5 will expect a fixed batch size and might reject the model. I learned this the hard way when my first export failed with a cryptic shape mismatch error.

The trained model ends up as a ~5KB file. You'll place it in MQL5\Files or MQL5\Experts\YourEA\ — anywhere the FileOpen function can reach. I prefer MQL5\Files\ONNX\ to keep things organized, especially if you have multiple models for different symbols. The exact path matters: MT5 searches in MQL5\Files\ by default, not the Experts folder. If you put it in the wrong place, OnnxCreateFromFile will return INVALID_HANDLE with error 5116.

Data Normalization: The Silent Killer

Here's where most people screw up. In Python, you probably scaled your features to mean=0, std=1 using something like StandardScaler. That scaler object has mean_ and scale_ attributes. You MUST record those values and hardcode them in your MQL5 EA. If you don't, the model will receive raw feature values that are completely out of distribution, and your probabilities will be garbage — often stuck at 0.5 or 0.99 regardless of market conditions.

I learned this the hard way during my first test. The model output 0.97 on every single bar because the input features were 10x larger than anything it saw during training. Save yourself the headache: export the scaler parameters to a CSV or just hardcode them directly. Here's how I do it in Python:

import numpy as np
from sklearn.preprocessing import StandardScaler

# After fitting scaler on training data
scaler = StandardScaler()
# ... fit scaler on your training features ...

# Export to a simple text file
np.savetxt("scaler_mean.txt", scaler.mean_, fmt="%.8f")
np.savetxt("scaler_std.txt", scaler.scale_, fmt="%.8f")
print("Scaler parameters saved.")

Then in MQL5, you read those files in OnInit() or hardcode the values as constants. I prefer hardcoding because file I/O adds another failure point. Just copy the numbers into a global array:

double scaler_mean[7] = {0.12, 0.08, 0.05, 1.23, -0.34, 1.01, 0.02};
double scaler_std[7]  = {1.45, 1.67, 1.89, 0.56, 2.10, 0.78, 1.33};

These numbers are fake — you'll need your own from your training data. Don't skip this step. It's the single most common reason ONNX models fail in production.

Integrating ONNX into Your MQL5 EA

Now the fun part: making the EA call the model. You need three things: load the ONNX session, prepare input tensors, and run inference. Here's the relevant code snippet you'd drop into your EA's OnInit() and OnTick():

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long onnxHandle = INVALID_HANDLE;
long inputTensorId, outputTensorId;
ulong inputCount, outputCount;

//+------------------------------------------------------------------+
//| OnInit()                                                         |
//+------------------------------------------------------------------+
int OnInit()
{
   // Load the ONNX model
   onnxHandle = OnnxCreateFromFile("trend_filter.onnx", 0);
   if(onnxHandle == INVALID_HANDLE)
   {
      Print("Failed to create ONNX model. Error: ", GetLastError());
      return INIT_FAILED;
   }
   
   // Set input and output shapes
   const long inputShape[] = {1, 7};  // batch=1, features=7
   const long outputShape[] = {1, 1};
   OnnxSetInputShape(onnxHandle, 0, inputShape);
   OnnxSetOutputShape(onnxHandle, 0, outputShape);
   
   // Get tensor IDs
   inputTensorId = OnnxGetTensorID(onnxHandle, 0);
   outputTensorId = OnnxGetTensorID(onnxHandle, 0);
   
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| OnTick() - compute trend probability                             |
//+------------------------------------------------------------------+
double GetTrendProbability()
{
   if(onnxHandle == INVALID_HANDLE) return -1.0;
   
   // Prepare feature vector (normalized using training scaler)
   float features[7];
   // Assume we stored scaler_mean[7] and scaler_std[7] as global arrays
   features[0] = (float)(((Close[0] - Close[5]) / Close[5] * 100.0 - scaler_mean[0]) / scaler_std[0]);
   features[1] = (float)(((Close[0] - Close[10]) / Close[10] * 100.0 - scaler_mean[1]) / scaler_std[1]);
   features[2] = (float)(((Close[0] - Close[20]) / Close[20] * 100.0 - scaler_mean[2]) / scaler_std[2]);
   features[3] = (float)(((iATR(NULL,0,14,0) / Close[0] * 100.0) - scaler_mean[3]) / scaler_std[3]);
   features[4] = (float)(((Close[0] - iMA(NULL,0,50,0,MODE_SMA,PRICE_CLOSE,0)) / Close[0] * 100.0 - scaler_mean[4]) / scaler_std[4]);
   features[5] = (float)((((double)Volume[0] / (iMA(NULL,0,20,0,MODE_SMA,PRICE_VOLUME,0) > 0 ? iMA(NULL,0,20,0,MODE_SMA,PRICE_VOLUME,0) : 1.0)) - scaler_mean[5]) / scaler_std[5]);
   features[6] = (float)(((Close[0] - Open[0]) / Open[0] * 100.0 - scaler_mean[6]) / scaler_std[6]);
   
   // Run inference
   float outputData[1];
   if(!OnnxRun(onnxHandle, inputTensorId, features, outputTensorId, outputData))
   {
      Print("ONNX inference failed. Error: ", GetLastError());
      return -1.0;
   }
   
   return (double)outputData[0];  // probability 0..1
}

//+------------------------------------------------------------------+
//| Use in your entry logic                                          |
//+------------------------------------------------------------------+
void OnTick()
{
   double trendProb = GetTrendProbability();
   if(trendProb < 0.0) return;  // error
   
   // Only trade if probability exceeds threshold
   double threshold = 0.65;  // tune this
   if(trendProb > threshold)
   {
      // Your entry logic here
   }
   else
   {
      // Stay flat or exit
   }
}

A few things I learned the hard way:

  • Data normalization: Your model was trained on normalized features. In Python, you probably scaled them to mean=0, std=1. You must replicate that exact scaling in MQL5. I hardcoded the scaling factors from my training set into the EA. If you don't, your probabilities will be garbage.
  • Error handling: OnnxCreateFromFile can fail if the file path is wrong or the model is corrupt. Always check the handle. I've seen "Error 5116" (model not found) more times than I care to admit. Double-check the file path — MQL5\Files\trend_filter.onnx is the default search location, not the Experts folder.
  • Performance: Inference takes about 0.1-0.3 ms per call on a modern CPU. That's fine for H1 or H4, but if you're running on M1 ticks, you might want to cache the result per bar and only recompute on new bar. I use a simple static datetime lastBarTime = 0; check in OnTick() to avoid redundant model calls.
  • Input tensor shape: The OnnxSetInputShape call is critical. If your model expects a 2D tensor [batch, features], you must set it correctly. Mismatched shapes will cause inference to fail silently — the OnnxRun returns false, but the error code might not tell you why.

Parameter Optimization in MQL5

You'll want to expose the threshold as an input parameter so you can optimize it in the Strategy Tester. Add this to your EA:

input double TrendThreshold = 0.65;  // ONNX trend probability threshold (0.0 to 1.0)

Then in your entry logic, use TrendThreshold instead of the hardcoded 0.65. This lets you run optimization runs to find the best value for your specific strategy and symbol. I typically optimize between 0.5 and 0.9 in steps of 0.05. Values below 0.5 tend to let too many false signals through, while values above 0.85 often miss too many good trades.

Backtest Setup and Results

I tested this filter on a simple breakout EA that buys when price closes above the 20-period high and sells when it closes below the 20-period low. The base EA without any filter had a profit factor of 1.12 on EURUSD H1 from 2021-2023 — not great, with lots of drawdown. The strategy worked in trending markets but got destroyed during ranges, which is exactly what we're trying to fix.

After adding the

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles