基于LSTM的趋势预测在趋势跟踪策略中的应用·进阶篇
🧠

基于LSTM的趋势预测在趋势跟踪策略中的应用·进阶篇

(2/3)· 从MT5抽数到Python训模再到EA回测,趋势过滤的实战链路一次打通

实战向进阶 第 2/3 篇
直接用当前ADX判断趋势强度,往往在高点才确认趋势已近尾声,入场就吃震荡。本篇改用LSTM预测未来ADX,提前过滤弱趋势信号,让趋势跟踪少接劣质单。

◍ LSTM训练时的三个坑和ONNX导出

用LSTM做行情序列预测,核心是把数据切成(样本数, 时间步长, 特征数)的三维张量。比如 time_step=5,就是用周一到周五前5根K线的ADX、RSI、Stationary三个特征,去推第6个时间点的 output 值。模型自己学每个时间步、每个特征的权重,最后由全连接层吐出一个数。 训练前最容易被忽略的是顺序。数据必须按时间排,train_test_split 里 shuffle 一定要设 False,否则把未来样本混进训练块,回测看着漂亮,实盘就是前瞻性偏差的典型翻车。示例里 X_train, X_eval = train_test_split(X, y, test_size=0.2, shuffle=False) 这行才是能用的写法。 模型别堆太深。外汇和贵金属这种样本量有限的高风险品种,LSTM(50) 加一个 Dense(1) 跑 50 个 epoch、batch_size=100 已经够用。叠太多层和单元,验证集损失容易和训练集脱节,过拟合后参数偏移会很大。 评估阶段看 MSE。代码跑完会打印 Evaluation Loss: 57.405677795410156,这就是验证集预测值和真实值的均方误差。判断标准只有一条:验证损失和训练损失接近、且相对 output 均值平方不算离谱,模型才没白训。 要让 MT5 能调用,得先转 ONNX。LSTM 原生不支持直接导出,需先把模型改成函数式并显式定义输入输出,输入维度第一维设 None,这样任意批量样本都能喂进去逐条出预测,不用固定每次推理的样本数。

MQL5 / C++
class="kw">import numpy as np
class="kw">import matplotlib.pyplot as plt
from sklearn.model_selection class="kw">import train_test_split
from tensorflow.keras.models class="kw">import Sequential
from tensorflow.keras.layers class="kw">import LSTM, Dense
# Assume data is your DataFrame already loaded with the specified columns and a time-based index
# data.columns should include [&class="macro">#x27;ADX&class="macro">#x27;, &class="macro">#x27;RSI&class="macro">#x27;, &class="macro">#x27;Stationary&class="macro">#x27;, &class="macro">#x27;output&class="macro">#x27;]
# --- Step class="num">1: Data Preparation ---
time_step = class="num">5
# Select features and target
features = [&class="macro">#x27;ADX&class="macro">#x27;, &class="macro">#x27;RSI&class="macro">#x27;, &class="macro">#x27;Stationary&class="macro">#x27;]
target = &class="macro">#x27;output&class="macro">#x27;
# --- Step class="num">2: Create sequences for LSTM input ---
def create_sequences(data, target_col, time_step):
    """
    Create sequences of length time_step from the DataFrame.
    data: DataFrame of input features and target.
    target_col: Name of the target column.
    Returns: X, y arrays suitable for LSTM.
    """
    X, y = [], []
    feature_cols = data.columns.drop(target_col)
    for i in range(len(data) - time_step):
        seq_x = data.iloc[i:i+time_step][feature_cols].values
        # predict target at the next time step after the sequence
        seq_y = data.iloc[i+time_step][target_col]
        X.append(seq_x)
        y.append(seq_y)
    class="kw">return np.array(X), np.array(y)
# Create sequences
X, y = create_sequences(data, target_col=target, time_step=time_step)
# --- Step class="num">3: Split into training and evaluation sets ---
# Use a simple class="num">80/class="num">20 split for training and evaluation
X_train, X_eval, y_train, y_eval = train_test_split(X, y, test_size=class="num">0.2, shuffle=False)
# --- Step class="num">4: Build the LSTM model ---
n_features = len(features)  # number of features per time step
model = Sequential()
model.add(LSTM(class="num">50, input_shape=(time_step, n_features)))  # LSTM layer with class="num">50 units
model.add(Dense(class="num">1))  # output layer for regression
model.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mse&class="macro">#x27;)
model.summary()
# --- Step class="num">5: Train the model ---
epochs = class="num">50
batch_size = class="num">100
history = model.fit(
    X_train, y_train,
    epochs=epochs,
    batch_size=batch_size,
    validation_data=(X_eval, y_eval)
)
# --- Step class="num">6: Evaluate the model ---
eval_loss = model.evaluate(X_eval, y_eval)
print(f"Evaluation Loss: {eval_loss}")
# --- Step class="num">7: Generate Predictions and Plot ---
# Generate predictions on the evaluation set
predictions = model.predict(X_eval).flatten()
# Create a plot for predictions vs actual values
plt.figure(figsize=(class="num">12, class="num">6))
plt.plot(predictions, label=&class="macro">#x27;Predicted Output&class="macro">#x27;, class="type">color=&class="macro">#x27;red&class="macro">#x27;)
plt.plot(y_eval, label=&class="macro">#x27;Actual Output&class="macro">#x27;, class="type">color=&class="macro">#x27;blue&class="macro">#x27;)
plt.title(&class="macro">#x27;LSTM Predictions vs Actual Output&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Sample Index&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Output Value&class="macro">#x27;)
plt.legend()
plt.show()
评估亏损值:class="num">57.405677795410156
class="kw">import tensorflow as tf
class="kw">import tf2onnx
# Define the input shape based on your LSTM requirements: (time_step, n_features)
time_step = class="num">5
n_features = class="num">3

把 Keras 时序模型导成 ONNX 给 MT5 用

在 MT5 里跑深度学习信号,第一步是把训练好的 Keras 时序模型转成 ONNX,这样绕开 Python 依赖、直接进推理引擎。下面这段是转换的核心代码,输入维度必须和训练时一致,否则转换会直接报错。 代码里 input_signature 的 TensorSpec 用了 (None, time_step, n_features),None 对应批量维,time_step 和 n_features 得是你特征工程里定的真实值,比如 30 根 K 线、每根 5 个特征就是 (None,30,5)。opset=15 是 ONNX 算子集版本,低于 13 对动态形状支持会出问题。 转完落地的文件叫 regression2024.onnx,MT5 的 ONNX 模块加载它时路径要写绝对路径。外汇和贵金属波动受消息面扰动大,模型历史拟合不等于未来概率,实盘前务必用 2023 以后的样本外数据回测。

MQL5 / C++
inputs = tf.keras.Input(shape=(time_step, n_features), name="input")
outputs = model(inputs)  
functional_model = tf.keras.Model(inputs=inputs, outputs=outputs)
input_signature = (
    tf.TensorSpec((None, time_step, n_features), dtype=tf.float32, name="input"),
)
output_path = "regression2024.onnx"
onnx_model, _ = tf2onnx.convert.from_keras(
    functional_model,
    input_signature=input_signature,  # matching the input signature
    opset=class="num">15,                      
    output_path=output_path
)
print(f"Model successfully converted to ONNX at {output_path}")

「把LSTM塞进金叉EA的骨架里」

把训练好的ONNX模型丢进 /MQL5/Files 之后,回到MetaEditor,我们直接在经典金叉趋势跟踪框架上改。快慢均线交叉跟趋势,价格穿过慢线离场给跟踪止损留空间——这套逻辑本身不新,但接下来要让LSTM在交叉信号出现时才跑一次预测,过滤掉弱趋势。 先声明全局变量:模型输入/输出维度参数、存数据的双维数组,以及一个模型句柄管加载和取结果。OnInit() 里除了初始化RSI、ADX,还要核对MQL5侧声明的维度跟Python训练时一致,否则32位精度对不上会直接初始化失败。 每根新K线触发更新函数,循环5个时间步把数据转成浮点塞进全局数组,顺序必须是历史在前、新数据追加在后。OnTick() 只在新K线跑检查,避免同根K线重复计算;当magic编号持仓全平,buypos和sellpos重置为0,防止已有仓位还开新单。 模型推理不在每根K线跑,只在MA交叉且无持仓时调用,把预测ADX值写进out数组。低于阈值视为趋势弱不开仓,高于才入场——回测里这能省掉大量无意义评估,XAUUSD 1h上用15/25周期快慢线配0.01手,外汇贵金属杠杆高,实盘前务必自己跑一遍验证阈值。

MQL5 / C++
class="macro">#include <Trade/Trade.mqh>
class=class="str">"cmt">//XAU - 1h.
CTrade trade;
input class="type">int MaPeriodsFast = class="num">15;
input class="type">int MaPeriodsSlow = class="num">25;
input class="type">int MaPeriods = class="num">200;
input class="type">class="kw">double lott = class="num">0.01;
class="type">ulong buypos = class="num">0, sellpos = class="num">0;
input class="type">int Magic = class="num">0;
class="type">int barsTotal = class="num">0;
class="type">int handleMaFast;
class="type">int handleMaSlow;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,class="num">0,MODE_SMA,PRICE_CLOSE);
   handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,class="num">0,MODE_SMA,PRICE_CLOSE);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
  }

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
  class="type">int bars = iBars(_Symbol,PERIOD_CURRENT);
  class=class="str">"cmt">//Beware, the last element of the buffer list is the most recent data, not [class="num">0]
  if (barsTotal!= bars){
    barsTotal = bars;
    class="type">class="kw">double maFast[];
    class="type">class="kw">double maSlow[];
    CopyBuffer(handleMaFast,BASE_LINE,class="num">1,class="num">2,maFast);
    CopyBuffer(handleMaSlow,BASE_LINE,class="num">1,class="num">2,maSlow);
    class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

◍ 持仓清理与交叉信号的执行顺序

这段逻辑里最容易被忽视的是平仓判断必须排在开仓之前。代码先用 iClose 取当前周期前一根收盘价 lastClose,只有当买持仓存在且收盘价跌破慢线 maSlow[1] 时才平多;卖持仓则是收盘价站上慢线才平空。顺序错了就可能在同根 K 线上既平又开,造成重复下单。 快慢线死叉与金叉的判定只看相邻两根:maFast[1]>maSlow[1] 且 maFast[0]<maSlow[0] 为死叉倾向平多开空,反之金叉倾向平空开多;同时要求 buypos==sellpos(都为 0 或同号)才调用 executeBuy / executeSell,避免已有同向仓时追单。 持仓票据有效性靠 OnTradeTransaction 回写。订单新增时若 Magic 匹配,BUY 单ticket 写入 buypos,SELL 单写入 sellpos;主循环里再用 PositionSelectByTicket 配合 POSITION_MAGIC 校验,票号失效或非本 EA 魔法则把 buypos/sellpos 归零,防止残票干扰下一轮信号。 executeSell / executeBuy 里用 SymbolInfoDouble 取实时买价并 NormalizeDouble 到品种小数位,再走 trade.Sell / trade.Buy,开仓后立刻用 trade.ResultOrder 拿订单号回填。外汇与贵金属杠杆高,这类自动平仓逻辑若魔数设错可能误平手动仓,上 MT5 前先开模拟盘跑一轮验证。

MQL5 / C++
class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, class="num">1);
class=class="str">"cmt">//The order below matters
if(buypos>class="num">0&& lastClose<maSlow[class="num">1]) trade.PositionClose(buypos);
if(sellpos>class="num">0 &&lastClose>maSlow[class="num">1])trade.PositionClose(sellpos);
if (maFast[class="num">1]>maSlow[class="num">1]&&maFast[class="num">0]<maSlow[class="num">0]&&buypos ==sellpos)executeBuy();
if(maFast[class="num">1]<maSlow[class="num">1]&&maFast[class="num">0]>maSlow[class="num">0]&&sellpos ==buypos) executeSell();
if(buypos>class="num">0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
   buypos = class="num">0;
   }
if(sellpos>class="num">0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
   sellpos = class="num">0;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert trade transaction handling function                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result) {
   if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
      COrderInfo order;
      if (order.Select(trans.order)) {
         if (order.Magic() == Magic) {
            if (order.OrderType() == ORDER_TYPE_BUY) {
               buypos = order.Ticket();
            } else if (order.OrderType() == ORDER_TYPE_SELL) {
               sellpos = order.Ticket();
            }
         }
      }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Execute sell trade function                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void executeSell() {      
     class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
     bid = NormalizeDouble(bid,_Digits);
     trade.Sell(lott,_Symbol,bid);  
     sellpos = trade.ResultOrder();  
     }     
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Execute buy trade function                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void executeBuy() {

把 LSTM 模型塞进 EA 的初始化与取数

EA 启动时先把交易魔术码设好,再挂四条指标句柄:快慢 SMA 各一条,ADX 与 RSI 周期都写死 14。注意 ADX 默认取主线(缓冲区 0),和原文注释里的『-3』无关,那是别的写法残留,照抄容易误导。 ONNX 模型从编译进资源的 regression2024.onnx 读入,输入张量形状锁成 {1,5,3}、输出 {1,1},也就是用最近 5 根 K 线的 3 个特征跑出 1 个预测值。若 OnnxSetInputShape 或 OnnxSetOutputShape 任一返回 false,直接打印错误码并释放句柄返回 -1,EA 起不来。 getData 里用 CopyBuffer 从 handleAdx、handleRsi 各拽 5 个值,偏移从 1 开始——意味着跳过当前未闭合的这根 K 线,只用已闭合的 5 根。外汇与贵金属杠杆高,模型信号只是概率倾向,真要挂单前建议先开 MT5 用策略测试器跑一遍这句取数逻辑看是否越界。

MQL5 / C++
class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
ask = NormalizeDouble(ask,_Digits);
trade.Buy(lott,_Symbol,ask);
buypos = trade.ResultOrder();
}
class="macro">#resource "\\Files\\regression2024.onnx" as class="type">uchar lstm_onnx[]
class="type">float data[class="num">1][class="num">5][class="num">3];
class="type">float out[class="num">1][class="num">1];
class="type">long lstmHandle = INVALID_HANDLE;
const class="type">long input_shape[] = {class="num">1,class="num">5,class="num">3};
const class="type">long output_shape[]={class="num">1,class="num">1};
class="type">int handleMaFast;
class="type">int handleMaSlow;
class="type">int handleAdx;    class=class="str">"cmt">// Average Directional Movement Index - class="num">3
class="type">int handleRsi;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {class=class="str">"cmt">//Initialize model
   trade.SetExpertMagicNumber(Magic);
   handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,class="num">0,MODE_SMA,PRICE_CLOSE);
   handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,class="num">0,MODE_SMA,PRICE_CLOSE);  
   handleAdx=iADX(_Symbol,PERIOD_CURRENT,class="num">14);class=class="str">"cmt">//Average Directional Movement Index - class="num">3
   handleRsi = iRSI(_Symbol,PERIOD_CURRENT,class="num">14,PRICE_CLOSE);
    class=class="str">"cmt">// Load the ONNX model
   lstmHandle = OnnxCreateFromBuffer(lstm_onnx, ONNX_DEFAULT);
   class=class="str">"cmt">//--- specify the shape of the input data
   if(!OnnxSetInputShape(lstmHandle,class="num">0,input_shape))
     {
       Print("OnnxSetInputShape failed, error ",GetLastError());
       OnnxRelease(lstmHandle);
       class="kw">return(-class="num">1);
     }
class=class="str">"cmt">//--- specify the shape of the output data
   if(!OnnxSetOutputShape(lstmHandle,class="num">0,output_shape))
     {
       Print("OnnxSetOutputShape failed, error ",GetLastError());
       OnnxRelease(lstmHandle);
       class="kw">return(-class="num">1);
     }
   if (lstmHandle == INVALID_HANDLE)
   {
       Print("Error creating model OnnxCreateFromBuffer ", GetLastError());
       class="kw">return(INIT_FAILED);
   }
   class="kw">return(INIT_SUCCEEDED);
   }
class="type">void getData(){
     class="type">class="kw">double rsi[];
     class="type">class="kw">double adx[];
     CopyBuffer(handleAdx,class="num">0,class="num">1,class="num">5,adx);
     CopyBuffer(handleRsi,class="num">0,class="num">1,class="num">5,rsi);
让小布替你跑这套回测诊断
这些LSTM信号过滤与样本外回测的诊断维度,小布盯盘的AIGC已内置,打开对应品种页即可看到胜率和盈亏比的变化曲线,你只需核对参数边界。

常见问题

当前ADX高常意味着趋势发挥完毕,追入易接末尾震荡;预测未来值可提前识别强度上升段,过滤掉趋势不足时的信号。
目前小布内置的是通用趋势强度诊断与回测视图,自定义模型需先导出信号到MT5,再由小布做盘口层面的聚合呈现。
对小时级而言一年外样本能暴露过拟合,但是否充分取决于品种波动机制,建议交叉多个年份分段验证。
RSI本身有界不必强差分,但需检查其与原收益率的协整关系,避免引入伪平稳特征拖垮LSTM记忆单元。