English Русский Español Deutsch 日本語
preview
基于LSTM的趋势预测在趋势跟踪策略中的应用

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

MetaTrader 5交易系统 |
521 2
Zhuo Kai Chen
Zhuo Kai Chen

概述

长短期记忆网络(LSTM)是一种特殊的循环神经网络(RNN),其设计初衷是通过有效捕捉数据中的长期依赖关系,并解决传统RNN存在的梯度消失问题,从而实现对时序数据的高效建模。本文将系统阐述如何利用LSTM进行未来趋势预测,进而提升趋势跟踪策略的实战表现。具体内容涵盖这些模块:LSTM关键概念介绍与发展契机、从MetaTrader 5平台提取数据、在Python中构建并训练模型、将机器学习模型嵌入MQL5中、基于统计回测的结果分析与改进方向。


契机

趋势跟踪策略在趋势明显的市场中能实现盈利,但在震荡市场中表现欠佳——此时策略往往以高价买入、低价卖出。学术研究表明,包括"黄金交叉"在内的经典趋势跟踪策略,在历史长周期、多市场及多时间框架下均展现出有效性。尽管这类策略的绝对收益可能并不突出,但其盈利稳定性已得到验证。该类策略的核心盈利来源在于极端行情,其单次盈利幅度显著超过平均亏损。通过"收紧止损+让利润奔跑"的机制,策略实现了低胜率但高盈亏比的交易特征。

LSTM是RNN的改进版本,专门用于捕捉时序数据中的长期依赖关系。其通过记忆单元实现信息的长期存储,有效解决了传统RNN的梯度消失问题。这种对历史序列信息的存储与调用能力,使LSTM在时间序列预测和趋势研判任务中表现卓越。对于回归问题,LSTM可精准建模输入特征与连续输出之间的时序关联,尤其适用于金融市场的预测场景。

本文旨在探索LSTM在趋势回归分析中的应用,通过预测未来趋势方向,过滤掉趋势强度不足时的劣质交易信号。其契机基于以下假设:趋势跟踪策略在趋势明显的市场中表现优于无趋势市场。

我们选用平均趋向指数(ADX)作为趋势强度量化工具,因其是衡量当前市场趋势性的主流指标。与传统用法不同,本研究将尝试预测ADX的未来值而非直接使用其当前值——这是因为高ADX值往往意味着趋势已接近尾声,此时入场可能错失最佳时机。

ADX计算方法:

ADX公式


数据准备与预处理

在获取数据前,需明确所需数据类型。我们计划使用多个特征构建回归模型,以预测未来ADX值。这些特征包括:反映市场当前相对强弱的RSI、作为收盘价的平稳化表征值的最近K线收益率、与预测目标直接相关的ADX值本身。请注意,这里我们仅阐述特征选择逻辑。您可以自定义特征组合,但是需要确保其合理性与平稳性。我们将采用2020年1月1日至2024年1月1日的小时级数据进行模型训练,并以2024年1月1日至2025年1月1日的数据作为样本外测试集。

明确数据需求后,让我们构建EA实现数据采集。

我们将使用该文章介绍的CFileCSV类,将数组数据以字符串形式保存为CSV文件。此过程的代码非常简洁,如下所示:

#include <FileCSV.mqh>
CFileCSV csvFile;

int barsTotal = 0;
int handleRsi;
int handleAdx;
string headers[] = {
    "time",
    "ADX",
    "RSI",
    "Stationary"
};
string data[1000000][4];
int indexx = 0;
vector xx;

input string fileName = "XAU-1h-2020-2024.csv";
input bool SaveData = true;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {//Initialize model
   handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);
   handleAdx = iADX(_Symbol,PERIOD_CURRENT,14);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if (!SaveData) return;
   if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI))
     {
      //Write the header
      csvFile.WriteHeader(headers);
      //Write data rows
      csvFile.WriteLine(data);
      //Close the file
      csvFile.Close();
     }
   else
     {
      Print("File opening error!");
     }

  }
  
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);
  
  if (barsTotal!= bars){
     barsTotal = bars;
     double rsi[];
     double adx[];
     CopyBuffer(handleAdx,0,1,1,adx);
     CopyBuffer(handleRsi,0,1,1,rsi);
     data[indexx][0] =(string)TimeTradeServer();
   data[indexx][1] = DoubleToString(adx[0], 2); 
   data[indexx][2] = DoubleToString(rsi[0], 2); 
   data[indexx][3] = DoubleToString((iClose(_Symbol,PERIOD_CURRENT,1)-iOpen(_Symbol,PERIOD_CURRENT,1))/iClose(_Symbol,PERIOD_CURRENT,1),3);
   indexx++;
   }
 }

该EA用于跟踪记录指定交易品种的RSI和ADX数值。EA通过iRSI()和iADX()函数获取当前RSI与ADX值,并将时间戳、指标值及平稳化数据存储至CSV文件。CSV文件头包含:"time"、"ADX"、"RSI"和"Stationary"。当启用SaveData选项时,EA会在去初始化时将数据写入指定文件(路径由fileName参数定义)。系统在每个tick更新时追踪新数据,仅在K线数量变化时执行存储操作。

在策略测试器中以单次测试模式运行该EA。测试完成后,数据文件将被保存至以下路径:/Tester/Agent-sth000/MQL5/Files。

接下来,我们进入Python环境进行数据预处理,为机器学习模型训练做准备。

本研究采用监督学习框架,模型通过学习带标签的数据来预测目标值。训练过程中通过调整特征运算权重,最小化误差损失以生成最终输出。 

建议使用未来10个ADX值的均值作为训练标签。该方法确保入场时趋势尚未完全形成,同时避免选取过于遥远的未来值导致信号失效。通过未来10个ADX值的均值反映后续数根K线的趋势强度,使入场点能有效捕获后续方向性行情的利润。

import pandas as pd
data = pd.read_csv('XAU-1h-2020-2024.csv', sep=';')
data= data.dropna().set_index('time')
data['output'] = data['ADX'].shift(-10)
data = data[:-10]
data['output']= data['output'].rolling(window=10).mean()
data = data[9:]

该代码读取CSV文件后,按分号(;)将数据分隔到不同的列中。随后,代码删除所有空行,并将“time”列设为索引,以确保数据按时间顺序排列,便于训练。接下来,新建名为“output”的列,用于计算未来10个ADX值的均值。之后,代码再次删除剩余的空行(因部分行可能无法提供足够的未来ADX值来计算输出)。


模型训练

LSTM结构图

上图展示了LSTM在训练阶段的核心目标:我们希望输入张量的维度(样本数, 时间步长, 特征数),其中time_steps表示用过去多少个时间点的数据来预测下一个值。例如,可用周一至周四的数据预测周五的某种结果。LSTM通过算法识别时间序列中的模式以及特征与结果之间的关系。它构建一层或多层神经网络,每层包含大量权重单元(神经元),对每个特征和每个时间步长赋予权重,最终输出预测值。

为简化操作,您只需运行下方代码,训练过程将自动完成。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

# Assume data is your DataFrame already loaded with the specified columns and a time-based index
# data.columns should include ['ADX', 'RSI', 'Stationary', 'output']

# --- Step 1: Data Preparation ---
time_step = 5

# Select features and target
features = ['ADX', 'RSI', 'Stationary']
target = 'output'

# --- Step 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)
    return np.array(X), np.array(y)

# Create sequences
X, y = create_sequences(data, target_col=target, time_step=time_step)

# --- Step 3: Split into training and evaluation sets ---
# Use a simple 80/20 split for training and evaluation
X_train, X_eval, y_train, y_eval = train_test_split(X, y, test_size=0.2, shuffle=False)

# --- Step 4: Build the LSTM model ---
n_features = len(features)  # number of features per time step

model = Sequential()
model.add(LSTM(50, input_shape=(time_step, n_features)))  # LSTM layer with 50 units
model.add(Dense(1))  # output layer for regression

model.compile(optimizer='adam', loss='mse')

model.summary()

# --- Step 5: Train the model ---
epochs = 50
batch_size = 100

history = model.fit(
    X_train, y_train,
    epochs=epochs,
    batch_size=batch_size,
    validation_data=(X_eval, y_eval)
)

使用上述代码训练时,请注意以下要点:

  1. 数据预处理:务必保证数据按时间顺序排列。否则,在将数据分割为固定时间步长(time_steps)的区块时,可能会导致前瞻性偏差。

  2. 训练-测试划分:拆分训练集与测试集时,不要打乱数据。必须保持时间顺序,避免前瞻性偏差。

  3. 模型复杂度:对于时间序列,尤其在数据量有限时,无需堆叠过多层、神经元或迭代次数。过度复杂化模型易导致过拟合或高偏差参数。示例中的设置已经足够。 

随后,我们可以使用验证集评估模型在未见数据上的表现,以此衡量其预测准确性。

    # --- Step 6: Evaluate the model ---
    eval_loss = model.evaluate(X_eval, y_eval)
    print(f"Evaluation Loss: {eval_loss}")
    # --- Step 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=(12, 6))
    plt.plot(predictions, label='Predicted Output', color='red')
    plt.plot(y_eval, label='Actual Output', color='blue')
    plt.title('LSTM Predictions vs Actual Output')
    plt.xlabel('Sample Index')
    plt.ylabel('Output Value')
    plt.legend()
    plt.show()

    该代码应输出验证集预测结果与真实值的均方误差(MSE),输出格式如下:

    可视化评估结果

    评估亏损值:57.405677795410156

    计算方法:

    均方误差

    其中,n为样本量;yi 为第i个样本的预测值,y^i为每个评估结果的实际值。

    由计算过程可见,可通过比较模型损失与预测目标均值平方值,判断相对损失是否过高。同时需确保验证集损失与训练集损失相近,以此验证模型未对训练数据产生过拟合。

    最后,为使模型适配MQL5环境,需将其保存为ONNX格式。由于LSTM模型不直接支持ONNX转换,需先将模型保存为函数式架构,并明确定义其输入和输出格式。完成此步骤后,即可导出为ONNX文件,供后续MQL5调用。

    import tensorflow as tf
    import tf2onnx
    
    # Define the input shape based on your LSTM requirements: (time_step, n_features)
    time_step = 5
    n_features = 3
    
    # Create a new Keras Input layer matching the shape of your data
    inputs = tf.keras.Input(shape=(time_step, n_features), name="input")
    
    # Pass the input through your existing sequential model
    outputs = model(inputs)  
    functional_model = tf.keras.Model(inputs=inputs, outputs=outputs)
    
    # Create an input signature that matches the defined input shape
    input_signature = (
        tf.TensorSpec((None, time_step, n_features), dtype=tf.float32, name="input"),
    )
    
    output_path = "regression2024.onnx"
    
    # Convert the functional model to ONNX format
    onnx_model, _ = tf2onnx.convert.from_keras(
        functional_model,
        input_signature=input_signature,  # matching the input signature
        opset=15,                         
        output_path=output_path
    )
    
    print(f"Model successfully converted to ONNX at {output_path}")

    需注意,此处输入格式设为"None",表示模型可接受任意数量的样本输入。模型将自动为每个样本生成对应的预测结果,从而灵活适配不同批量的数据处理需求。


    构建EA

    保存好ONNX模型文件后,需将其复制至/MQL5/Files目录以备后续调用。

    我们返回MetaEditor环境。将基于经典的金叉趋势跟踪策略进行扩展开发。该策略与我此前在前一篇机器学习文章 中实现的版本一致。基本逻辑是通过快慢两条移动平均线的交叉信号判断趋势。当两条移动平均线交叉时产生交易信号,交易方向跟随快速移动平均线,因此称为“趋势跟踪”。当价格穿越慢速移动平均线时触发离场信号,为跟踪止损留出更大空间。完整代码如下:

    #include <Trade/Trade.mqh>
    //XAU - 1h.
    CTrade trade;
    
    input int MaPeriodsFast = 15;
    input int MaPeriodsSlow = 25;
    input int MaPeriods = 200;
    input double lott = 0.01;
    
    ulong buypos = 0, sellpos = 0;
    input int Magic = 0;
    int barsTotal = 0;
    int handleMaFast;
    int handleMaSlow;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       trade.SetExpertMagicNumber(Magic);
       handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE);
       handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE);  
       return(INIT_SUCCEEDED);
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
    
      }
      
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
      int bars = iBars(_Symbol,PERIOD_CURRENT);
      //Beware, the last element of the buffer list is the most recent data, not [0]
      if (barsTotal!= bars){
         barsTotal = bars;
         double maFast[];
         double maSlow[];
         CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast);
         CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow);
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
         //The order below matters
         if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos);
         if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos);   
         if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&buypos ==sellpos)executeBuy(); 
         if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos ==buypos) executeSell();
         if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          buypos = 0;
          }
         if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          sellpos = 0;
          }
        }
     }
    
    //+------------------------------------------------------------------+
    //| Expert trade transaction handling function                       |
    //+------------------------------------------------------------------+
    void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const 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();
                    }
                }
            }
        }
    }
    
    //+------------------------------------------------------------------+
    //| Execute sell trade function                                      |
    //+------------------------------------------------------------------+
    void executeSell() {      
           double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
           bid = NormalizeDouble(bid,_Digits);
           trade.Sell(lott,_Symbol,bid);  
           sellpos = trade.ResultOrder();  
           }    
    
    //+------------------------------------------------------------------+
    //| Execute buy trade function                                       |
    //+------------------------------------------------------------------+
    void executeBuy() {
           double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
           ask = NormalizeDouble(ask,_Digits);
           trade.Buy(lott,_Symbol,ask);
           buypos = trade.ResultOrder();
    }

    关于回测策略的验证方法及选择建议,本文不再展开详述。更多详细内容可参阅我此前发布的机器学习相关文章,链接已附此处

    现在,我们将基于该框架运行LSTM模型。

    首先,需声明这些全局变量:指定模型输入/输出维度的参数和用于存储输入/输出数据的双维数组。另外,我们声明一个模型句柄,用于管理数据加载与预测结果提取的流程。该配置可确保模型与输入/输出变量间的数据流及交互逻辑的正确性。

    #resource "\\Files\\regression2024.onnx" as uchar lstm_onnx[]
    
    float data[1][5][3];
    float out[1][1];
    long lstmHandle = INVALID_HANDLE;
    const long input_shape[] = {1,5,3};
    const long output_shape[]={1,1};

    接下来,在 OnInit()函数中,我们将初始化相关技术指标,如 RSI、ADX及ONNX模型。初始化过程中,需验证MQL5中声明的输入/输出维度是否与Python函数式模型中定义的规格一致。该步骤可确保数据格式兼容性,避免模型初始化错误,从而确保按照预期格式正确处理数据。

    int handleMaFast;
    int handleMaSlow;
    int handleAdx;     // Average Directional Movement Index - 3
    int handleRsi;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {//Initialize model
       trade.SetExpertMagicNumber(Magic);
       handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE);
       handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE);   
       handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3
       handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);
        // Load the ONNX model
       lstmHandle = OnnxCreateFromBuffer(lstm_onnx, ONNX_DEFAULT);
       //--- specify the shape of the input data
       if(!OnnxSetInputShape(lstmHandle,0,input_shape))
         {
          Print("OnnxSetInputShape failed, error ",GetLastError());
          OnnxRelease(lstmHandle);
          return(-1);
         }
    //--- specify the shape of the output data
       if(!OnnxSetOutputShape(lstmHandle,0,output_shape))
         {
          Print("OnnxSetOutputShape failed, error ",GetLastError());
          OnnxRelease(lstmHandle);
          return(-1);
         }
       if (lstmHandle == INVALID_HANDLE)
       {
          Print("Error creating model OnnxCreateFromBuffer ", GetLastError());
          return(INIT_FAILED);
       }
       return(INIT_SUCCEEDED);
      }

    接下来,我们声明一个用于更新输入数据的函数,该函数会在每根新K线生成时触发。此函数通过循环遍历时间步长(本例中为5),将对应数据存入全局多维数组中。过程中会将数据转换为浮点型,以满足ONNX模型所需的32位精度要求。此外,该函数会确保多维数组的数据顺序正确:历史数据在前,新数据按生成顺序追加。这样保证了模型接收到的数据是按时间顺序排列的。

    void getData(){
         double rsi[];
         double adx[];
         CopyBuffer(handleAdx,0,1,5,adx);
         CopyBuffer(handleRsi,0,1,5,rsi);
         for (int i =0; i<5; i++){
         data[0][i][0] = (float)adx[i]; 
         data[0][i][1] = (float)rsi[i]; 
         data[0][i][2] = (float)((iClose(_Symbol,PERIOD_CURRENT,5-i)-iOpen(_Symbol,PERIOD_CURRENT,5-i))/iClose(_Symbol,PERIOD_CURRENT,5-i));
         }
    }

    最后,在OnTick()函数中实现交易逻辑。

    该函数会确保仅在新K线生成时触发后续交易逻辑检查。此机制可避免同一K线周期内的重复计算或交易操作,同时保证模型预测均基于每个时间步长的完整数据。

    int bars = iBars(_Symbol,PERIOD_CURRENT);
    if (barsTotal!= bars){
       barsTotal = bars;

    当EA的magic编号关联的持仓全部平仓后,该代码会将buypos和sellpos变量重置为0。这两个变量用于确保在生成入场信号前,买方和卖方持仓均为空。通过在无持仓时重置这些变量,可避免系统在已有持仓的情况下误开新仓。

    if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
     buypos = 0;
     }
    if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
     sellpos = 0;
     }

    我们通过这行代码运行ONNX模型,该模型接收输入数据并将预测结果输出至out数组。此操作仅在初始入场信号形成时执行,而非每根新K线都触发。这种策略可以节省计算资源,使回测更高效——避免在无入场信号期间进行不必要的模型评估。

    OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);

    当前交易逻辑为:当均线(MA)交叉发生且当前无持仓时,运行模型获取预测的ADX值。如果该值低于设定阈值,则判定为趋势性较弱,避免开仓;若高于阈值,则执行入场。完整OnTick()函数的实现如下:

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
      int bars = iBars(_Symbol,PERIOD_CURRENT);
      if (barsTotal!= bars){
         barsTotal = bars;
         double maFast[];
         double maSlow[];
         double adx[];
         CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast);
         CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow);
         CopyBuffer(handleAdx,0,1,1,adx);
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
         //The order below matters
         if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos);
         if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos);   
         if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){
            getData();
            OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);
            if(out[0][0]>threshold)executeSell();}
         if(maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){
            getData();
            OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);
            if(out[0][0]>threshold)executeBuy();}
         if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          buypos = 0;
          }
         if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          sellpos = 0;
          }
        }
     }


    统计回测

    完成所有功能实现后,我们可编译EA并在策略测试器中验证结果。本次将对XAUUSD(黄金兑美元)在2024年1月1日至2025年1月1日期间的1小时K线图进行样本外测试。首先,我们将运行原始回测策略作为比较基准。预期搭载LSTM模型的EA在此期间表现将优于基准策略。 

    组合设置

    组合参数

    组合净值曲线

    回测结果

    现在,我们将对搭载LSTM模型的EA进行回测,ADX阈值设定为30,该值通常被视为强趋势信号的临界点。

    LSTM设置

    LSTM参数

    LSTM净值曲线

    LSTM结果

    通过对比两组回测结果发现,LSTM模型过滤了约70%的原始交易,同时将盈亏比从1.48提升至1.52。该模型还表现出比基准策略更高的线性回归(LR)相关性,表明其有助于提升整体收益的稳定性。

    在回测机器学习模型时,需要特别注意的是,与参数影响较小的简单策略不同,模型内部参数是决定性能的关键因素。因此,不同训练数据可能会导致参数结果产生显著差异。此外,一次性使用全部历史数据训练并不理想,由于样本量过大且多数数据缺乏时效性。为此,建议在此类场景下采用滑动窗口法进行回测。如果整个回测历史中的样本量有限,正如我在前文关于CatBoost模型的讨论所述,则更适合使用扩展窗口回测法。

    以下是演示图例:

    滑动窗口法

    滑动窗口回测法采用固定大小的历史数据窗口,按时间顺序向前滚动。每当新增数据点时,系统会自动移除最旧的数据点,从而保持测试窗口大小恒定,以评估策略在不同时段的性能表现。

    扩展窗口法

    扩展窗口回测法初始采用固定大小的数据窗口,但随着新数据点的加入,窗口会动态扩展以纳入新增数据,从而在不断增大的数据集上持续测试策略性能。

    执行滑动窗口回测时,只需重复本文所述流程,并将各阶段结果合并为统一数据集。以下是2015年1月1日至2025年1月1日期间的滑动窗口回测表现:

    滑动窗口回测法

    度量指标:

    盈亏比:1.24
    最大回撤:-250.56
    平均盈利:12.02
    平均亏损:-5.20
    胜率:34.81%

    该结果表现优异,但仍具备进一步的优化空间。


    反思

    EA的性能与模型预测能力直接相关。若需提升EA表现,需重点考量以下因素:

    1. 回测策略优势:原始交易信号中需有足够比例具备显著优势,方能支撑后续筛选的合理性。
    2. 数据利用:通过分析输入特征的贡献度,挖掘被忽视的市场无效性指标,可获取超额收益机会。
    3. 模型选择:明确待解决问题类型(分类/回归),并精准调校训练参数。
    4. 预测目标:与其直接预测交易结果,不如聚焦间接影响最终结果的变量(如本文所述方法)。

    前文中,我系统探讨了零散交易者可落地的机器学习技术。旨在激发读者自身的实践创新——该领域的技术突破永无止境。机器学习并非高深莫测,而是一种思维模式。需理解优势,构建预测模型,并严格验证假设。随着实践的深入,认知将逐步升华。



    结论

    本文首先阐述了采用LSTM模型进行趋势预测的契机,同时解析了平均趋向指数(ADX)与长短期记忆网络(LSTM)的核心原理。随后,我们通过MetaTrader 5平台获取数据并完成预处理,在Python环境中完成模型训练。接着,我们演示了EA的构建流程,并深入剖析了回测结果。最后,介绍了滑动窗口回测与扩展窗口回测两种方法论,并在结语部分分享了策略优化的深度思考。


    文件表

    文件名 文件使用
    FileCSV.mqh 用于将数据存储为CSV格式的Include文件
    LSTM_Demonstration.ipynb 用于训练LSTM模型的python文件
    LSTM-TF-XAU.mq5 集成LSTM模型的EA
    OHLC Getter.mq5 用于获取数据的EA


    本文由MetaQuotes Ltd译自英文
    原文地址: https://www.mql5.com/en/articles/16940

    附加的文件 |
    LSTM-Trend.zip (132.27 KB)
    最近评论 | 前往讨论 (2)
    an_tar
    an_tar | 17 7月 2025 在 15:47
    我不明白:压缩包中的 regression2024.onnx 模型本身在哪里?
    Zhuo Kai Chen
    Zhuo Kai Chen | 17 7月 2025 在 16:01
    an_tar #:
    我不明白:regression2024.onnx 模型本身在压缩包里的什么地方?

    你好,an_tar。

    正如文章中提到的,此类系统应通过滚动窗口回溯测试进行验证。我不想把我自 2008 年以来训练的所有模型都包含在内,以免文件太重。

    建议使用文章中介绍的框架来训练你自己的模型,以便与你个人的验证方法兼容。

    从头开始以 MQL5 实现 SHA-256 加密算法 从头开始以 MQL5 实现 SHA-256 加密算法
    长期以来,构建无 DLL 的加密货币兑换集成一直是一个挑战,但该解决方案为直接市场对接提供了一个完整的框架。
    MQL5 简介(第 10 部分):MQL5 中使用内置指标的初学者指南 MQL5 简介(第 10 部分):MQL5 中使用内置指标的初学者指南
    本文介绍如何使用 MQL5 中的内置指标,重点介绍如何使用基于项目的方法创建基于 RSI 的 EA 交易。您将学习获取和利用 RSI 值、处理流动性清扫以及使用图表对象增强交易可视化。此外,本文强调了有效的风险管理,包括设定基于百分比的风险、实施风险回报率以及应用风险修改来确保利润。
    迁移至 MQL5 Algo Forge(第 4 部分):使用版本和发布 迁移至 MQL5 Algo Forge(第 4 部分):使用版本和发布
    我们将继续开发 Simple Candles 和 Adwizard 项目,同时还将描述使用 MQL5 Algo Forge 版本控制系统和仓库的细节。
    在MQL5中自动化交易策略(第5部分):开发自适应交叉RSI交易套件策略 在MQL5中自动化交易策略(第5部分):开发自适应交叉RSI交易套件策略
    在本文中,我们开发了自适应交叉RSI交易套件系统。该系统使用周期为14和50的移动平均线交叉来产生信号,并由一个周期为14的RSI过滤器进行确认。该系统包含一个交易日过滤器、带注释的信号箭头,以及一个用于监控的实时仪表盘。 这种方法确保了自动化交易中的精确性和适应性。