English Русский Deutsch
preview
トレンドフォロー戦略のためのLSTMによるトレンド予測

トレンドフォロー戦略のためのLSTMによるトレンド予測

MetaTrader 5トレーディングシステム |
137 2
Zhuo Kai Chen
Zhuo Kai Chen

はじめに

長・短期記憶(LSTM: Long Short-Term Memory)は、長期的な依存関係を捉える能力に優れ、勾配消失問題にも対処できる、時系列データ処理に特化した再帰型ニューラルネットワーク(RNN: Recurrent Neural Network)の一種です。本記事では、LSTMを活用して将来のトレンドを予測し、トレンドフォロー型戦略のパフォーマンスを向上させる方法について解説します。内容は、主要な概念と開発の背景の紹介、MetaTrader 5からのデータ取得、そのデータを用いたPythonでのモデル学習、学習済みモデルのMQL5への統合、そして統計的なバックテストに基づく結果の分析と今後の展望までを含みます。


動機

直感的に言えば、トレンドフォロー戦略はトレンド相場では利益を上げやすい一方、レンジ相場では成績が悪くなりがちで、結果として高値で買って安値で売るという状況に陥ることがあります。学術的な研究では、ゴールデンクロスのような古典的なトレンドフォロー戦略が、長い歴史の中で複数の市場や時間枠にわたって有効であることが示されています。これらの戦略は爆発的な収益を生むものではないかもしれませんが、一貫した利益を生み出す傾向があります。トレンドフォロー戦略は、平均的な損失を大きく上回るような、極端な値動き(外れ値)から収益を得る傾向があります。このような戦略は、タイトなストップロスと「利益を伸ばす」というアプローチをとるため、勝率は低くなるものの、1回の取引あたりのリスクリワード比は高くなります。

LSTM(長・短期記憶)は、時系列データにおける長期的な依存関係を捉えるために設計された、特殊なタイプの再帰型ニューラルネットワーク(RNN)です。LSTMは長期間にわたって情報を保持できるメモリセルを備えており、従来のRNNが直面していた勾配消失問題を克服します。このように、シーケンスの初期段階の情報に長くアクセスできるため、LSTMは時系列予測やトレンド予測のタスクに非常に効果的です。回帰問題においては、LSTMは入力特徴量間の時間的な関係性をモデル化し、連続的な出力を高精度に予測することが可能です。これらの特性から、LSTMは予測タスクにおいて非常に高い効果を発揮します。

本稿の目的は、LSTMの性能をトレンド回帰に応用し、将来のトレンドを予測することで、トレンドの欠如によって生じる質の低い取引を事前に回避することにあります。これは、トレンドフォロー戦略が、トレンドのない相場よりもトレンドのある相場でより良いパフォーマンスを発揮するという仮説に基づいています。

現在のトレンドの強さを測る指標として最も一般的なものの一つがADX(Average Directional Index:平均方向性指数)です。本稿では、ADXの現在の値ではなく将来の値を予測することを目指します。なぜなら、ADXが高い状態は、すでにトレンドが発生しているか、あるいはその終盤である可能性が高く、エントリータイミングとしては遅れることが多いためです。

ADXは次のように計算されます。

ADX方程式


データの準備と前処理

データを取得する前に、まずどのようなデータが必要なのかを明確にする必要があります。今回は、将来のADX値を予測する回帰モデルをトレーニングするために、いくつかの特徴量を使用する予定です。具体的には、市場の現在の相対的な強さを示すRSI、終値の変動を定常的に捉えるための直近のローソク足の収益率、そして予測対象であるADX自体を含みます。これらの特徴量を選定した理由については直感的な説明を先に述べましたが、もちろん使用する特徴量は読者自身で選ぶことも可能です。ただし、それらの特徴量が合理的であり、かつ統計的に定常であることを確認する必要があります。モデルのトレーニングには、2020年1月1日から2024年1月1日までの1時間足データを使用し、2024年1月1日から2025年1月1日までの期間をサンプル外テストとして、モデルのパフォーマンスを検証する予定です。

取得すべきデータの定義が明確になったので、次はこのデータを取得するためのエキスパートアドバイザー(EA)を構築していきましょう。

配列を文字列としてCSVファイルに保存するためには、この記事で紹介したCFileCSVクラスを使用します。この処理のコードは非常にシンプルで、以下のようになります。

#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にデータが書き込まれます。EAはティックごとにデータの変化を監視し、バーの本数に変化があった場合に新しいデータを記録します。

ストラテジーテスターでこのEAを単体テストとして実行します。テストが完了すると、ファイルはパス「/Tester/Agent-sth000/MQL5/Files」に保存されます。

次に、Pythonでのデータ前処理に移り、機械学習モデルのトレーニング準備をおこないます。

今回は教師あり学習のアプローチを採用します。この手法では、ラベル付きデータをもとに、モデルが目的の出力を予測できるようにトレーニングされます。トレーニングの過程では、特徴量に対してさまざまな処理を行う中で、それらの重みが調整され、誤差損失が最小になるように学習が進み、最終的な出力が得られます。 

ラベルとしては、次の10本分のADXの平均値を使用することを提案します。この方法は、エントリー時点でトレンドがすでに完全に形成されているという事態を避けながら、かといって現在のシグナルから離れすぎた将来を予測してしまうことも防ぎます。ADXの今後10本分の平均を用いることで、トレンドが直近の複数バーにわたって継続しているかどうかを測定でき、エントリーポイントがその後の方向性のある動きを捉えやすくなります。

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値の平均を計算して格納します。この処理により、モデルは将来のトレンドの強さを予測できるようになります。


モデルの訓練

LSTM図

この図は、トレーニングプロセス中にLSTMが達成しようとしていることを示しています。入力データの形状は(sample_amount, time_steps, feature_amount)にする必要があります。ここでtime_stepsは、将来の値を予測するために使用する過去の時系列データの長さを表します。たとえば、月曜日から木曜日までのデータを使って、金曜日の結果を予測するようなケースが該当します。LSTMは、時系列内のパターンや、特徴量と結果との関係を認識するためのアルゴリズムを活用します。そして、1つまたは複数のニューラルネットワーク層を構築し、それぞれの層は多くの重みユニット(ニューロン)から構成され、各特徴量および各時点に対して重みを適用し、最終的な予測値を出力します。

なお、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)が次のように出力されるはずです。

    評価の可視化

    Evaluation Loss: 57.405677795410156

    計算式は次のとおりです。

    平均二乗誤差

    ここで、nはサンプルサイズ、yiは各サンプルに対する予測値、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に戻ります。ここでは、クラシックなトレンドフォロー戦略であるゴールデンクロスのシグナルロジックをベースに構築していきます。これは前回の機械学習記事でも実装したものと同じです。 基本的なロジックは、2本の移動平均線(短期と長期)に基づいています。これら2本の移動平均線がクロスしたときに売買シグナルが発生し、短期移動平均線の方向に従って取引をおこないます。そのため、「トレンドフォロー戦略」と呼ばれます。決済シグナルは、価格が長期移動平均線をクロスしたときに発生します。これにより、トレーリングストップにある程度の余裕を持たせることができます。以下に、戦略の完全なコードを示します。

    #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モデルの実行を試みます。

    まず、入力と出力の形状を指定するグローバル変数を宣言し、さらに入力データと出力データを格納するための多次元配列を2つ用意します。加えて、モデルのデータ取得や予測抽出の処理を管理するためのモデルハンドルも宣言します。これにより、モデルと入力・出力変数間の適切なデータの流れと連携が確保されます。

    #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);
      }

    次に、新しいバーごとに入力データを更新する関数を宣言します。この関数はtime_step(今回の場合は5)分ループし、それに対応するデータをグローバルの多次元配列に格納します。データはONNXモデルが期待する32ビット浮動小数点形式になるようにfloat型に変換されます。さらに、多次元配列の順序が正しく、古いデータが先に来て新しいデータが順番に追加されるように管理されます。これにより、モデルに時系列の正しい順序でデータが入力されることが保証されます。

    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()関数で取引ロジックを実装します。

    この関数は、新しいバーが形成されたときのみ以降の取引ロジックをチェックするようにしています。これにより、同じバー内で不要な再計算や取引処理が行われることを防ぎ、モデルの予測が各新しいタイムステップにおける完全なデータに基づくものとなるようにしています。

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

    このコードは、EAのマジックナンバーに該当するポジションが一切残っていない場合に、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配列に出力します。この処理は、新規エントリーシグナルが発生したときのみ実行され、すべての新しいバーごとに実行されるわけではありません。この方法により、不要なモデル評価を避けて計算リソースを節約でき、エントリーシグナルがない期間のバックテストをより効率的におこなうことができます。

    OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);

    取引ロジックは次のようになります。移動平均線のクロスが発生し、現在ポジションが開かれていない場合にモデルを実行して予測された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をコンパイルしてストラテジーテスターで結果を検証できます。2024年1月1日から2025年1月1日までの1時間足を対象に、XAUUSDのサンプル外テストを実施します。まずは元のベースとなるストラテジーを基準として動かします。この期間中、LSTMを組み込んだEAがベースラインを上回るパフォーマンスを示すことが期待されます。 

    バックボーン設定

    バックボーンパラメータ

    バックボーンエクイティカーブ

    バックボーンの結果

    次に、ADXが30であることが強いトレンドの指標として広く認識されているため、閾値を30に設定してLSTMを実装したEAのバックテストを実行してみましょう。

    LSTM設定

    LSTMパラメータ

    LSTMエクイティカーブ

    LSTMの結果

    両者の結果を比較すると、LSTM実装は元の取引の約70%をフィルタリングし、プロフィットファクターを1.48から1.52へ改善しました。また、ベースラインよりも高いLR相関を示しており、より安定した全体的なパフォーマンスに寄与していることが示唆されます。

    機械学習モデルのバックテストにおいては、モデル内部のパラメータが重要な決定要因であることを理解することが大切です。単純な戦略ではパラメータの影響が小さいこともありますが、学習データが異なればパラメータの結果も大きく変わる可能性があります。また、すべての過去データを一度に学習させるのは望ましくなく、サンプル数が多すぎて多くが時代遅れとなる恐れがあります。したがって、このような場合のバックテストにはスライディングウィンドウ法を用いることを推奨します。もし、私の前回のCatBoostモデルの記事で触れたように、限られたサンプル数しか存在しない場合は、拡張ウィンドウ法でのバックテストがより適しています。

    以下にデモ用の画像を示します。

    スライディングウィンドウ

    スライディングウィンドウバックテストとは、一定の期間(固定サイズ)の過去データを用いて戦略を検証し、そのウィンドウを時間の経過とともに前方にスライドさせていく方法です。新しいデータが追加されると、最も古いデータが除外され、一定のデータウィンドウの大きさを保ちながら異なる期間にわたる戦略のパフォーマンスを評価します。

    拡張ウィンドウ

    拡張ウィンドウバックテストは、最初に固定サイズのデータウィンドウで開始しますが、新しいデータが入るごとにそのウィンドウを拡大し、時間とともに徐々に大きくなっていくデータセットを使って戦略を検証していく方法です。

    スライディングウィンドウバックテストを行うには、本記事で説明したプロセスを繰り返し、その結果をまとめて一つのデータセットに統合すればよいだけです。以下は、2015年1月1日から2025年1月1日までのスライディングウィンドウバックテストのパフォーマンスです。

    スライディングウィンドウバックテスト

    メトリクス:

    Profit Factor: 1.24
    Maximum Drawdown: -250.56
    Average Win: 12.02
    Average Loss: -5.20
    Win Rate: 34.81%

    結果は素晴らしいものでしたが、さらに改善の余地があります。


    振り返り

    EAのパフォーマンスはモデルが示す予測力と直接相関します。EAを改善するために考慮すべき重要なポイントは以下の通りです。

    1. まず、バックボーンとなる戦略の優位性です。元のシグナルの大多数に優位性がなければ、フィルタリングを強化しても意味がありません。
    2. 次に、使用するデータです。特徴量の重要度を分析し、まだ知られていない市場の非効率性を突ける特徴量を見つけることが有効です。
    3. さらに、使うモデルの種類も重要です。分類問題か回帰問題か、何を解決しようとしているかを明確にし、適切な学習パラメータを選ぶことも欠かせません。
    4. そして、予測対象も工夫しましょう。直接的にトレードの結果を予測するのではなく、本記事で示したように最終結果に間接的に関連するものを狙うのが効果的です。

    これまでの私の記事では、小売トレーダーでもアクセス可能なさまざまな機械学習技術を試してきました。皆さんがここで得たアイデアを活用し、自分なりの独創的な手法を構築することを願っています。機械学習は難解なものではなく、手の届かないものでもありません。大事なのはマインドセットであり、優位性を理解し、予測モデルを構築し、仮説検証を地道におこなうことです。試行錯誤を繰り返すうちに、その理解は徐々に深まっていきます。



    結論

    本記事では、まずトレンド予測にLSTMを使う動機を紹介し、ADXとLSTMの基礎概念を解説しました。次に、MetaTrader 5からデータを取得・加工し、Pythonでモデルをトレーニングする流れを示しました。続いて、EAの構築とバックテスト結果の検証をおこないました。最後に、スライディングウィンドウバックテストと拡張ウィンドウバックテストの考え方を紹介し、振り返りのまとめで締めくくりました。


    ファイルの表

    ファイル名 ファイルの使用法
    FileCSV.mqh データをCSVに保存するためのインクルードファイル
    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のモデル自体がzipアーカイブのどこにあるのか?
    Zhuo Kai Chen
    Zhuo Kai Chen | 17 7月 2025 において 16:01
    an_tar #:
    regression2024.onnxモデル自体はzipアーカイブのどこにあるのでしょうか?

    こんにちは、an_tarさん。

    記事にあるように、このタイプのシステムはローリングウィンドウ・バックテストで検証されます。2008年以降に学習したモデルをすべて含めるとファイルが重くなってしまうので、このようにしました。

    記事で紹介されているフレームワークを使って、ご自身の検証方法に適合するように独自のモデルをトレーニングすることをお勧めします。

    プライスアクション分析ツールキットの開発(第11回):Heikin Ashi Signal EA プライスアクション分析ツールキットの開発(第11回):Heikin Ashi Signal EA
    MQL5は、ユーザーの好みに合わせてカスタマイズ可能な自動売買システムを開発するための無限の可能性を提供します。複雑な数値計算も実行できることをご存知でしょうか。この記事では、自動売買戦略として日本の平均足手法を紹介します。
    知っておくべきMQL5ウィザードのテクニック(第53回):MFI (Market Facilitation Index) 知っておくべきMQL5ウィザードのテクニック(第53回):MFI (Market Facilitation Index)
    MFI(Market Facilitation Index、マーケットファシリテーションインデックス)は、ビル・ウィリアムズによる指標の一つで、出来高と連動した価格変動の効率性を測定することを目的としています。いつものように、本記事では、ウィザードアセンブリシグナルクラスの枠組みにおいて、このインジケーターのさまざまなパターンを検証し、それに基づいたテストレポートおよび分析結果を紹介します。
    PythonとMQL5を使用した特徴量エンジニアリング(第3回):価格の角度(2)極座標 PythonとMQL5を使用した特徴量エンジニアリング(第3回):価格の角度(2)極座標
    この記事では、あらゆる市場における価格レベルの変化を、それに対応する角度の変化へと変換する2回目の試みをおこないます。今回は、前回よりも数学的に洗練されたアプローチを採用しました。得られた結果は、アプローチを変更した判断が正しかった可能性を示唆しています。本日は、どの市場を分析する場合でも、極座標を用いて価格レベルの変化によって形成される角度を意味のある方法で計算する方法についてご説明します。
    MQL5での取引戦略の自動化(第5回):Adaptive Crossover RSI Trading Suite戦略の開発 MQL5での取引戦略の自動化(第5回):Adaptive Crossover RSI Trading Suite戦略の開発
    この記事では、14期間および50期間の移動平均クロスオーバーをシグナルとして使用し、14期間RSIフィルターで確認するAdaptive Crossover RSI Trading Suiteシステムを開発します。本システムには取引日フィルター、注釈付きのシグナル矢印、監視用のリアルタイムダッシュボードが含まれており、このアプローチにより自動取引の精度と適応性が向上します。