A moving-average crossover EA is the canonical "starter strategy" — cheap to backtest, easy to understand, and famously mediocre on its own because it generates many false signals in ranging markets. This article adds a LightGBM trend filter that the EA consults before acting on a crossover signal: only take the trade if the filter predicts the trend will persist.
The base EA (without filter)
Standard: buy when fast MA crosses above slow MA, sell when it crosses below. We'll keep this exact signal generation — we just wrap it with a yes/no decision from the ONNX model.
Training the trend filter
For each historical MA crossover signal, label whether it would have been profitable (1) or not (0). Use a fixed-horizon exit rule, e.g., 50 bars after entry. Features:
- Slope of slow MA over last 20 bars (proxy for prevailing direction).
- Distance from price to fast MA, normalized by ATR.
- Realized volatility over last 50 bars.
- Time since last crossover (sometimes crossovers cluster — usually bad).
- Higher-timeframe trend direction (H4/D1 MA slope).
train_filter.py
import lightgbm as lgb
from onnxmltools import convert_lightgbm
from onnxmltools.convert.common.data_types import FloatTensorType
# X: features at crossover. y: 1 if 50-bar return was profitable.
params = {"objective": "binary", "learning_rate": 0.05,
"num_leaves": 31, "max_depth": 6}
booster = lgb.train(params, lgb.Dataset(X, label=y), num_boost_round=200)
n_features = X.shape[1]
onx = convert_lightgbm(
booster,
initial_types=[("input", FloatTensorType([None, n_features]))],
target_opset=17,
)
with open("trend_filter.onnx", "wb") as f:
f.write(onx.SerializeToString())
Using the filter in MQL5
ma_cross_with_filter.mq5
#resource "models\trend_filter.onnx" as uchar FilterModel[]
input double InpFilterThreshold = 0.55;
long hFilter;
double FilterProbability()
{
matrixf in(1, 12); vectorf out(2);
// fill 12 features as training did, then:
OnnxRun(hFilter, ONNX_NO_CONVERSION, in, out);
return out[1]; // probability of class 1 (profitable)
}
void OnTick()
{
// ... existing crossover detection ...
if(BullishCrossover && FilterProbability() > InpFilterThreshold)
PlaceBuy();
if(BearishCrossover && FilterProbability() > InpFilterThreshold)
PlaceSell();
}
What to expect from the filter
- Trade count drops 40–60%.
- Win rate improves by 5–15 percentage points.
- Maximum drawdown improves materially — often the biggest practical win.
- Total profit may go up or down depending on whether the filter is removing more losers than winners.