English Deutsch
preview
PatchTST機械学習アルゴリズムによる24時間の値動きの予測

PatchTST機械学習アルゴリズムによる24時間の値動きの予測

MetaTrader 5トレーディング | 28 8月 2024, 10:24
33 0
Shashank Rai
Shashank Rai

はじめに

私が初めてPatchTSTというアルゴリズムに出会ったのは、Huggingface.coで時系列予測に関連するAIの進歩を調べ始めたときでした。大規模言語モデル(LLM)を扱ったことのある人なら誰でも知っているように、Transformerの発明は、自然言語、画像、ビデオ処理用のツールを開発する上でゲームチェンジャーとなりました。しかし、時系列ではどうでしょう。ただ置き去りにされるものなのでしょうか。それとも、ほとんどの研究は単に密室でおこなわれているのでしょうか。時系列の予測にTransformerをうまく適用した新しいモデルがたくさんあることがわかりました。この記事では、そのような実装のひとつを見てみましょう。

PatchTSTで印象的なのは、モデルの訓練が非常に速く、訓練されたモデルをMQLで使用するのが非常に簡単なことです。自分がニューラルネットワークの概念には疎いことは認めます。しかし、このプロセスを経て、この記事で概説されているMQL5用のPatchTSTの実装に取り組むことで、このような複雑なニューラルネットワークがどのように開発され、トラブルシューティングされ、訓練され、使用されるのかについて学び、理解する上で大きな飛躍を遂げたように感じました。やっと歩けるようになったばかりの子供をプロのサッカーチームに入れ、ワールドカップ決勝で勝利のゴールを決めることを期待するようなものです。


PatchTSTの概要

PatchTSTを発見してから、その設計を説明した論文「A Time Series is Worth 64 Words:Long-term Forecasting with Transformers」を調べ始めました。タイトルは興味深いものでした。論文を読み進めるうちに、これは魅力的な構造だなと思いました。ずっと知りたいと思っていた要素がたくさんあります。当然、それを試して、予想がどのように機能するかを確かめたかったです。以下は、私がこのアルゴリズムにさらに興味を持った理由です。

  • PatchTSTを使用して、始値、高値、安値、終値を予測することができます。PatchTSTでは、始値、高値、安値、終値、そしてボリュームまで、すべてのデータをそのまま与えることができると感じました。すべてのデータは「パッチ」と呼ばれるものに変換されるため、データのパターンを見つけることが期待できます。パッチとは何かについては、この記事のもう少し後で詳しく説明します。今のところは、パッチが魅力的で、予測をより良いものにするのに役立つということを知っておくことが重要です。
  • PatchTSTで必要なデータ前処理は最小限です。アルゴリズムをさらに掘り下げていくと、著者が「RevIn」と呼ばれるものを使用していることに気づきました。これは逆インスタンス正規化です。RevInは「REVERSIBLE INSTANCE NORMALIZATION FOR ACCURATE TIME-SERIES FORECASTING AGAINST DISTRIBUTION SHIFT」というタイトルの論文に由来します。RevInは、時系列予測における分布シフトの問題に取り組もうとしています。アルゴリズムトレーダーとして、訓練したEAが市場を予測できなくなり、パラメータの再最適化と更新を余儀なくされたときの感覚は、あまりにもよく分かります。RevInは同じことをするための方法だと考えてください。 

  • このメソッドは基本的に、渡されたデータを以下の式を使用して正規化します。

    x = (x - mean) / std

    そして、モデルが予測をおこなう必要があるときには、逆の性質を利用してデータを非正規化します。

    x = x * std + mean

    RevInにはaffine_biasという別のプロパティもあります。最も簡単な言葉で言えば、これはデータセットに存在する歪度や尖度などを考慮した学習可能なパラメータです。 

    x = x * affine_weight + affine_bias

    PatchTSTの構造を要約すると以下のようになります。 

    Input Data -> RevIn -> Series Decomposition -> Trend Component -> PatchTST Backbone -> TSTiEncoder -> Flatten_Head -> Trend Forecaster -> Residual Component -> Add Trend and Residual -> Final Forecast

    私たちのデータはMT5を使用して取得されると理解しています。RevInの仕組みについても説明しました。

    例えば、H1時間枠で80,000バーのEURUSDデータを取り出したとします。ちょうど13年分のデータです。PatchTSTでは、データを「パッチ」と呼ばれるものに分割します。例えて言うなら、パッチはVision Transformers (ViT)が画像に対して機能するのと似ていますが、時系列データに対して適応させたものだと考えてください。つまり、例えばパッチの長さが16であれば、各パッチには16の連続した価格値が含まれることになります。これは、時系列の小さな塊を一度に見るようなもので、大域的パターンを考慮する前に、モデルが局所的なパターンに集中するのに役立ちます。

    次に、パッチは配列の順序を保持するための位置エンコーディングを含み、モデルが配列内の各パッチの位置を記憶するのを助けます。

    Transformerは、正規化されエンコードされたパッチをエンコーダー層のスタックに通します。各エンコーダー層には、Multi-Head Attention層とフィードフォワード層があります。Multi-Head Attention層は、モデルが入力シーケンスの異なる部分に注意することを可能にし、フィードフォワード層は、モデルがデータの複雑な非線形変換を学習することを可能にします。

    最後に、トレンドと残差成分があります。 同じパッチング、正規化、位置エンコード、Transformer層が、トレンド成分と残差成分の両方に適用されます。次に、トレンド成分と残差成分の出力を合計し、最終的な予測を作成します。


    PatchTST公式リポジトリの問題

    PatchTSTの公式リポジトリはGitHubの以下のリンクにあります。 PatchTST (ICLR 2023)。教師ありと教師なしの2種類があります。この記事では、教師あり学習のアプローチを使用します。ご存知のように、MQL5でどんなモデルでも使用するためには、ONNX形式に変換する方法が必要です。しかし、PatchTSTの著者はこのことを考慮していません。このモデルをMQL5で動作させるために、彼らのベースコードに以下の修正を加えなければなりませんでした。 

    オリジナルコード 

    class PatchTST_backbone(nn.Module):
        def __init__(self, c_in:int, context_window:int, target_window:int, patch_len:int, stride:int, max_seq_len:Optional[int]=1024, 
                     n_layers:int=3, d_model=128, n_heads=16, d_k:Optional[int]=None, d_v:Optional[int]=None,
                     d_ff:int=256, norm:str='BatchNorm', attn_dropout:float=0., dropout:float=0., act:str="gelu", key_padding_mask:bool='auto',
                     padding_var:Optional[int]=None, attn_mask:Optional[Tensor]=None, res_attention:bool=True, pre_norm:bool=False,                          
                     store_attn:bool=False,
                     pe:str='zeros', learn_pe:bool=True, fc_dropout:float=0., head_dropout = 0, padding_patch = None,
                     pretrain_head:bool=False, head_type = 'flatten', individual = False, revin = True, affine = True, subtract_last = False,
                     verbose:bool=False, **kwargs):
            
            super().__init__()
            
            # RevIn
            self.revin = revin
            if self.revin: self.revin_layer = RevIN(c_in, affine=affine, subtract_last=subtract_last)
            
            # Patching
            self.patch_len = patch_len
            self.stride = stride
            self.padding_patch = padding_patch
            patch_num = int((context_window - patch_len)/stride + 1)
            if padding_patch == 'end': # can be modified to general case
                self.padding_patch_layer = nn.ReplicationPad1d((0, stride)) 
                patch_num += 1
            
            # Backbone 
            self.backbone = TSTiEncoder(c_in, patch_num=patch_num, patch_len=patch_len, max_seq_len=max_seq_len,
                                    n_layers=n_layers, d_model=d_model, n_heads=n_heads, d_k=d_k, d_v=d_v, d_ff=d_ff,
                                    attn_dropout=attn_dropout, dropout=dropout, act=act, key_padding_mask=key_padding_mask, padding_var=padding_var,
                                    attn_mask=attn_mask, res_attention=res_attention, pre_norm=pre_norm, store_attn=store_attn,
                                    pe=pe, learn_pe=learn_pe, verbose=verbose, **kwargs)
    
            # Head
            self.head_nf = d_model * patch_num
            self.n_vars = c_in
            self.pretrain_head = pretrain_head
            self.head_type = head_type
            self.individual = individual
    
            if self.pretrain_head: 
                self.head = self.create_pretrain_head(self.head_nf, c_in, fc_dropout) # custom head passed as a partial func with all its kwargs
            elif head_type == 'flatten': 
                self.head = Flatten_Head(self.individual, self.n_vars, self.head_nf, target_window, head_dropout=head_dropout)
            
        
        def forward(self, z):                                                                   # z: [bs x nvars x seq_len]
            # norm
            if self.revin: 
                z = z.permute(0,2,1)
                z = self.revin_layer(z, 'norm')
                z = z.permute(0,2,1)
                
            # do patching
            if self.padding_patch == 'end':
                z = self.padding_patch_layer(z)
            z = z.unfold(dimension=-1, size=self.patch_len, step=self.stride)                   # z: [bs x nvars x patch_num x patch_len]
            z = z.permute(0,1,3,2)                                                              # z: [bs x nvars x patch_len x patch_num]
            
            # model
            z = self.backbone(z)                                                                # z: [bs x nvars x d_model x patch_num]
            z = self.head(z)                                                                    # z: [bs x nvars x target_window] 
            
            # denorm
            if self.revin: 
                z = z.permute(0,2,1)
                z = self.revin_layer(z, 'denorm')
                z = z.permute(0,2,1)
            return z

    上記のコードが主要なバックボーンです。ご覧のように、このコードでは行の中でUnfoldという関数を使用しています。 

     z = z.unfold(dimension=-1, size=self.patch_len, step=self.stride)                   # z: [bs x nvars x patch_num x patch_len]

    Unfoldの変換はONNXではサポートされていません。次のようなエラーが表示されます。 

    Unsupported: ONNX export of operator Unfold, input size not accessible. Please feel free to request support or submit a pull request on PyTorch GitHub: https://github.com/pytorch/pytorch/issues

    だから、コードのこの部分をこう置き換える必要がありました。 

    # Manually unfold the input tensor
        batch_size, n_vars, seq_len = z.size()
        patches = []
        for i in range(0, seq_len - self.patch_len + 1, self.stride):
            patches.append(z[:, :, i:i+self.patch_len])

    上記の置き換えは、ニューラルネットワークの訓練にforループを使用しているため、少し効率が悪いことに注意してください。その非効率性は、何度もエポックを重ね、大規模なデータセットになればなるほど積み重なっていきますが、これは必要なことです。そうでなければ、モデルは単に変換に失敗し、MQL5で使用することができないからです。 

    私は特にこの問題を取り上げました。これが一番時間がかかりました。そして、この記事に添付されているzipファイルにあるpatchTST.pyというファイルにすべてをまとめました。これはモデルの訓練に使用するファイルです。 


    PythonでPatchTSTを使用するための要件

    このセクションでは、PythonでPatchTSTを使用するための必要条件について説明します。これらの要件をまとめると以下のようになります。 

    仮想環境を作成します。 

    python -m venv myenv

    仮想環境をアクティブにします(Windows)

    .\myenv\Scripts\activate
    

    この記事に添付されているzipファイルに含まれているrequirements.txtファイルをインストールします。 

    pip install -r requirements.txt

    具体的には、このプロジェクトを実行するために必要な条件は以下の通りです。 

    MetaTrader5
    pandas
    numpy
    torch
    plotly
    datetime


    モデル訓練コードの開発(ステップごと)

    以下のコードについては、ZIPファイルに含まれているJupyterノートブックPatchTST Step-By-Step.ipynbを使用して私と一緒に追うことができます。以下にその手順をまとめます。 

    1. 必要なライブラリのインポートMetaTrader 5、Pandas、Numpy、Torch、PatchTSTモデルなど、必要なライブラリをインポートします。

      # Step 1: Import necessary libraries
      import MetaTrader5 as mt5
      import pandas as pd
      import numpy as np
      import torch
      from torch.utils.data import TensorDataset, DataLoader
      from patchTST import Model as PatchTST
    2. MetaTrader 5の初期化とデータ取得:関数fetch_mt5_dataはMT5を初期化し、指定された銘柄、時間枠、バー数のデータをフェッチし、始値、高値、安値、終値の列を持つデータフレームを返します。

      # Step 2: Initialize and fetch data from MetaTrader 5
      def fetch_mt5_data(symbol, timeframe, bars):
          if not mt5.initialize():
              print("MT5 initialization failed")
              return None
      
          timeframe_dict = {
              'M1': mt5.TIMEFRAME_M1,
              'M5': mt5.TIMEFRAME_M5,
              'M15': mt5.TIMEFRAME_M15,
              'H1': mt5.TIMEFRAME_H1,
              'D1': mt5.TIMEFRAME_D1
          }
      
          rates = mt5.copy_rates_from_pos(symbol, timeframe_dict[timeframe], 0, bars)
          mt5.shutdown()
      
          df = pd.DataFrame(rates)
          df['time'] = pd.to_datetime(df['time'], unit='s')
          df.set_index('time', inplace=True)
          return df[['open', 'high', 'low', 'close']]
      
      # Fetch data
      data = fetch_mt5_data('EURUSD', 'H1', 80000)
    3. スライディングウィンドウを使用して予測データを準備する:関数prepare_forecasting_dataは、スライディングウィンドウ法を用いてデータセットを作成し、過去のデータ(X)とそれに対応する将来のデータ(y)のシーケンスを生成します。

      # Step 3: Prepare forecasting data using sliding window
      def prepare_forecasting_data(data, seq_length, pred_length):
          X, y = [], []
          for i in range(len(data) - seq_length - pred_length):
              X.append(data.iloc[i:(i + seq_length)].values)
              y.append(data.iloc[(i + seq_length):(i + seq_length + pred_length)].values)
          return np.array(X), np.array(y)
      
      seq_length = 168  # 1 week of hourly data
      pred_length = 24  # Predict next 24 hours
      
      X, y = prepare_forecasting_data(data, seq_length, pred_length)
    4. データを訓練セットとテストセットに分ける:データを訓練用とテスト用に分け、80%を訓練用、20%をテスト用とします。 

      # Step 4: Split data into training and testing sets
      split = int(len(X) * 0.8)
      X_train, X_test = X[:split], X[split:]
      y_train, y_test = y[:split], y[split:]
      
    5. データをPyTorchテンソルに変換する:NumPyの配列をPyTorchのテンソルに変換します。これはPyTorchでの訓練に必要です。トーチの手動シードを設定し、結果の再現性を高めます。 

      # Step 5: Convert data to PyTorch tensors
      X_train = torch.tensor(X_train, dtype=torch.float32)
      y_train = torch.tensor(y_train, dtype=torch.float32)
      X_test = torch.tensor(X_test, dtype=torch.float32)
      y_test = torch.tensor(y_test, dtype=torch.float32)
      
      torch.manual_seed(42)
    6. 計算デバイスを設定する:利用可能であればデバイスをCUDAに設定し、そうでなければCPUを使用します。これは、特に利用できる場合、訓練中にGPUアクセラレーションを活用するために不可欠です。

      # Step 6: Set device for computation
      device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      print(f"Using device: {device}")
    7. 訓練データ用のデータローダーを作成する:訓練データのバッチ処理とシャッフルをおこなうデータローダーを作成します。

      # Step 7: Create DataLoader for training data
      train_dataset = TensorDataset(X_train, y_train)
      train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
    8. モデルの構成クラスを定義する:PatchTSTモデルに必要なすべてのハイパーパラメータと設定を格納する構成クラスConfigを定義します。

      # Step 8: Define the configuration class for the model
      class Config:
          def __init__(self):
              self.enc_in = 4  # Adjusted for 4 columns (open, high, low, close)
              self.seq_len = seq_length
              self.pred_len = pred_length
              self.e_layers = 3
              self.n_heads = 4
              self.d_model = 64
              self.d_ff = 256
              self.dropout = 0.1
              self.fc_dropout = 0.1
              self.head_dropout = 0.1
              self.individual = False
              self.patch_len = 24
              self.stride = 24
              self.padding_patch = True
              self.revin = True
              self.affine = False
              self.subtract_last = False
              self.decomposition = True
              self.kernel_size = 25
      
      configs = Config()
    9. PatchTSTモデルを初期化する:定義された構成でPatchTSTモデルを初期化し、選択されたデバイスに移動します。

      # Step 9: Initialize the PatchTST model
      model = PatchTST(
          configs=configs,
          max_seq_len=1024,
          d_k=None,
          d_v=None,
          norm='BatchNorm',
          attn_dropout=0.1,
          act="gelu",
          key_padding_mask='auto',
          padding_var=None,
          attn_mask=None,
          res_attention=True,
          pre_norm=False,
          store_attn=False,
          pe='zeros',
          learn_pe=True,
          pretrain_head=False,
          head_type='flatten',
          verbose=False
      ).to(device)
    10. オプティマイザと損失関数を定義する:モデルを訓練するためのオプティマイザ(Adam)と損失関数(平均二乗誤差)を設定します。

      # Step 10: Define optimizer and loss function
      optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
      loss_fn = torch.nn.MSELoss()
      num_epochs = 100
    11. モデルを訓練する:指定されたエポック数にわたってモデルを訓練します。各バッチのデータに対して、モデルはフォワードパスを実行し、損失を計算し、勾配を計算するためにバックワードパスを実行し、モデルパラメータを更新します。

      # Step 11: Train the model
      for epoch in range(num_epochs):
          model.train()
          total_loss = 0
          for batch_X, batch_y in train_loader:
              optimizer.zero_grad()
              batch_X = batch_X.to(device)
              batch_y = batch_y.to(device)
      
              outputs = model(batch_X)
              outputs = outputs[:, -pred_length:, :4]
      
              loss = loss_fn(outputs, batch_y)
      
              loss.backward()
              optimizer.step()
              total_loss += loss.item()
      
          print(f"Epoch {epoch+1}/{num_epochs}, Loss: {total_loss/len(train_loader):.10f}")
    12. モデルをPyTorch形式で保存する:訓練済みモデルの状態辞書をファイルに保存します。このファイルを使用してpythonで直接予測をおこなうことができます。 

      # Step 12: Save the model in PyTorch format
      torch.save(model.state_dict(), 'patchtst_model.pth')
    13. ONNXエクスポート用のダミー入力を準備する:ONNX形式へのモデルのエクスポートに使用するダミーの入力テンソルを作成します。 

      # Step 13: Prepare a dummy input for ONNX export
      dummy_input = torch.randn(1, seq_length, 4).to(device)
    14. モデルをONNX形式にエクスポートする:訓練済みモデルをONNX形式にエクスポートします。 MQL5で予測をおこなうには、このファイルが必要になります。 

      # Step 14: Export the model to ONNX format
      torch.onnx.export(model, dummy_input, "patchtst_model.onnx",
                        opset_version=13,
                        input_names=['input'],
                        output_names=['output'],
                        dynamic_axes={'input': {0: 'batch_size'},
                                      'output': {0: 'batch_size'}})
      
      print("Model trained and saved in PyTorch and ONNX formats.")


    モデルの訓練結果

    以下は、モデルを訓練した結果です。 

    Epoch 1/100, Loss: 0.0000283705
    Epoch 2/100, Loss: 0.0000263274
    Epoch 3/100, Loss: 0.0000256321
    Epoch 4/100, Loss: 0.0000252389
    Epoch 5/100, Loss: 0.0000249340
    Epoch 6/100, Loss: 0.0000246715
    Epoch 7/100, Loss: 0.0000244293
    Epoch 8/100, Loss: 0.0000241942
    Epoch 9/100, Loss: 0.0000240157
    Epoch 10/100, Loss: 0.0000236776
    Epoch 11/100, Loss: 0.0000233954
    Epoch 12/100, Loss: 0.0000230437
    Epoch 13/100, Loss: 0.0000226635
    Epoch 14/100, Loss: 0.0000221875
    Epoch 15/100, Loss: 0.0000216960
    Epoch 16/100, Loss: 0.0000213242
    Epoch 17/100, Loss: 0.0000208693
    Epoch 18/100, Loss: 0.0000204956
    Epoch 19/100, Loss: 0.0000200573
    Epoch 20/100, Loss: 0.0000197222
    Epoch 21/100, Loss: 0.0000193516
    Epoch 22/100, Loss: 0.0000189223
    Epoch 23/100, Loss: 0.0000186635
    Epoch 24/100, Loss: 0.0000184025
    Epoch 25/100, Loss: 0.0000180468
    Epoch 26/100, Loss: 0.0000177854
    Epoch 27/100, Loss: 0.0000174621
    Epoch 28/100, Loss: 0.0000173247
    Epoch 29/100, Loss: 0.0000170032
    Epoch 30/100, Loss: 0.0000168594
    Epoch 31/100, Loss: 0.0000166609
    Epoch 32/100, Loss: 0.0000164818
    Epoch 33/100, Loss: 0.0000162424
    Epoch 34/100, Loss: 0.0000161265
    Epoch 35/100, Loss: 0.0000159775
    Epoch 36/100, Loss: 0.0000158510
    Epoch 37/100, Loss: 0.0000156571
    Epoch 38/100, Loss: 0.0000155327
    Epoch 39/100, Loss: 0.0000154742
    Epoch 40/100, Loss: 0.0000152778
    Epoch 41/100, Loss: 0.0000151757
    Epoch 42/100, Loss: 0.0000151083
    Epoch 43/100, Loss: 0.0000150182
    Epoch 44/100, Loss: 0.0000149140
    Epoch 45/100, Loss: 0.0000148057
    Epoch 46/100, Loss: 0.0000147672
    Epoch 47/100, Loss: 0.0000146499
    Epoch 48/100, Loss: 0.0000145281
    Epoch 49/100, Loss: 0.0000145298
    Epoch 50/100, Loss: 0.0000144795
    Epoch 51/100, Loss: 0.0000143969
    Epoch 52/100, Loss: 0.0000142840
    Epoch 53/100, Loss: 0.0000142294
    Epoch 54/100, Loss: 0.0000142159
    Epoch 55/100, Loss: 0.0000140837
    Epoch 56/100, Loss: 0.0000140005
    Epoch 57/100, Loss: 0.0000139986
    Epoch 58/100, Loss: 0.0000139122
    Epoch 59/100, Loss: 0.0000139010
    Epoch 60/100, Loss: 0.0000138351
    Epoch 61/100, Loss: 0.0000138050
    Epoch 62/100, Loss: 0.0000137636
    Epoch 63/100, Loss: 0.0000136853
    Epoch 64/100, Loss: 0.0000136191
    Epoch 65/100, Loss: 0.0000136272
    Epoch 66/100, Loss: 0.0000135552
    Epoch 67/100, Loss: 0.0000135439
    Epoch 68/100, Loss: 0.0000135200
    Epoch 69/100, Loss: 0.0000134461
    Epoch 70/100, Loss: 0.0000133950
    Epoch 71/100, Loss: 0.0000133979
    Epoch 72/100, Loss: 0.0000133059
    Epoch 73/100, Loss: 0.0000133242
    Epoch 74/100, Loss: 0.0000132816
    Epoch 75/100, Loss: 0.0000132145
    Epoch 76/100, Loss: 0.0000132803
    Epoch 77/100, Loss: 0.0000131212
    Epoch 78/100, Loss: 0.0000131809
    Epoch 79/100, Loss: 0.0000131538
    Epoch 80/100, Loss: 0.0000130786
    Epoch 81/100, Loss: 0.0000130651
    Epoch 82/100, Loss: 0.0000130255
    Epoch 83/100, Loss: 0.0000129917
    Epoch 84/100, Loss: 0.0000129804
    Epoch 85/100, Loss: 0.0000130086
    Epoch 86/100, Loss: 0.0000130156
    Epoch 87/100, Loss: 0.0000129557
    Epoch 88/100, Loss: 0.0000129013
    Epoch 89/100, Loss: 0.0000129018
    Epoch 90/100, Loss: 0.0000128864
    Epoch 91/100, Loss: 0.0000128663
    Epoch 92/100, Loss: 0.0000128411
    Epoch 93/100, Loss: 0.0000128514
    Epoch 94/100, Loss: 0.0000127915
    Epoch 95/100, Loss: 0.0000127778
    Epoch 96/100, Loss: 0.0000127787
    Epoch 97/100, Loss: 0.0000127623
    Epoch 98/100, Loss: 0.0000127452
    Epoch 99/100, Loss: 0.0000127141
    Epoch 100/100, Loss: 0.0000127229

    結果は次のように可視化できます。 

    100エポックを超える訓練の進歩

    また、エラーや警告のない次のような出力が得られ、モデルがONNX形式に正常に変換されたことがわかります。 

    Model trained and saved in PyTorch and ONNX formats.
    


    Pythonを使用したステップごとの予測生成

    では、予測コードを見てみましょう。 

    1. ステップ1 - 必要なライブラリのインポート:必要なライブラリをすべてインポートすることから始めます。
      # Import required libraries
      import MetaTrader5 as mt5
      import pandas as pd
      import numpy as np
      import torch
      from datetime import datetime, timedelta
      import plotly.graph_objects as go
      from plotly.subplots import make_subplots
      from patchTST import Model as PatchTST
    2. ステップ2 - Metatrader 5からデータを取得する:MetaTrader 5からデータを取得し、DataFrameに変換する関数を定義します。168本の先行バーを取得するのは、私たちのモデルで予測を得るために必要だからです。 
      # Function to fetch data from MetaTrader 5
      def fetch_mt5_data(symbol, timeframe, bars):
          if not mt5.initialize():
              print("MT5 initialization failed")
              return None
      
          timeframe_dict = {
              'M1': mt5.TIMEFRAME_M1,
              'M5': mt5.TIMEFRAME_M5,
              'M15': mt5.TIMEFRAME_M15,
              'H1': mt5.TIMEFRAME_H1,
              'D1': mt5.TIMEFRAME_D1
          }
      
          rates = mt5.copy_rates_from_pos(symbol, timeframe_dict[timeframe], 0, bars)
          mt5.shutdown()
      
          df = pd.DataFrame(rates)
          df['time'] = pd.to_datetime(df['time'], unit='s')
          df.set_index('time', inplace=True)
          return df[['open', 'high', 'low', 'close']]
      
      # Fetch the latest week of data
      historical_data = fetch_mt5_data('EURUSD', 'H1', 168)
    3. ステップ3 - 入力データの準備:最後のseq_length行のデータを取って、モデルの入力データを準備する関数を定義します。データを取得する際、今後24時間の予測をおこなうために必要なのは、過去168時間分の1hデータのみです。というのも、これがモデルの訓練方法だからです。 
      # Function to prepare input data
      def prepare_input_data(data, seq_length):
          X = []
          X.append(data.iloc[-seq_length:].values)
          return np.array(X)
      
      # Prepare the input data
      seq_length = 168  # 1 week of hourly data
      input_data = prepare_input_data(historical_data, seq_length)
      
    4. ステップ4 - 構成の定義:モデルのパラメータを設定するために構成クラスを定義します。これらの設定は、モデルの訓練に使用したものと同じです。 
      # Define the configuration class
      class Config:
          def __init__(self):
              self.enc_in = 4  # Adjusted for 4 columns (open, high, low, close)
              self.seq_len = seq_length
              self.pred_len = 24  # Predict next 24 hours
              self.e_layers = 3
              self.n_heads = 4
              self.d_model = 64
              self.d_ff = 256
              self.dropout = 0.1
              self.fc_dropout = 0.1
              self.head_dropout = 0.1
              self.individual = False
              self.patch_len = 24
              self.stride = 24
              self.padding_patch = True
              self.revin = True
              self.affine = False
              self.subtract_last = False
              self.decomposition = True
              self.kernel_size = 25
      
      # Initialize the configuration
      config = Config()
    5. ステップ5 - 訓練済みモデルの読み込み:訓練済みPatchTSTモデルを読み込む関数を定義します。これらはモデルの訓練に使用したのと同じ設定です。 
      # Function to load the trained model
      def load_model(model_path, config):
          model = PatchTST(
              configs=config,
              max_seq_len=1024,
              d_k=None,
              d_v=None,
              norm='BatchNorm',
              attn_dropout=0.1,
              act="gelu",
              key_padding_mask='auto',
              padding_var=None,
              attn_mask=None,
              res_attention=True,
              pre_norm=False,
              store_attn=False,
              pe='zeros',
              learn_pe=True,
              pretrain_head=False,
              head_type='flatten',
              verbose=False
          )
          model.load_state_dict(torch.load(model_path))
          model.eval()
          return model
      
      # Load the trained model
      model_path = 'patchtst_model.pth'
      device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      model = load_model(model_path, config).to(device)
      
    6. ステップ6 - 予測をおこなう:読み込まれたモデルと入力データを使用して予測をおこなう関数を定義します。
      # Function to make predictions
      def predict(model, input_data, device):
          with torch.no_grad():
              input_data = torch.tensor(input_data, dtype=torch.float32).to(device)
              output = model(input_data)
          return output.cpu().numpy()
      
      # Make predictions
      predictions = predict(model, input_data, device)
      
    7. ステップ7 - 後処理と可視化:予測を処理し、データフレームを作成し、Plotlyを使用して過去のデータと予測データを可視化します。
      # Ensure predictions have the correct shape
      if predictions.shape[2] != 4:
          predictions = predictions[:, :, :4]  # Adjust based on actual number of columns required
      
      # Check the shape of predictions
      print("Shape of predictions:", predictions.shape)
      
      # Create a DataFrame for predictions
      pred_index = pd.date_range(start=historical_data.index[-1] + pd.Timedelta(hours=1), periods=24, freq='H')
      pred_df = pd.DataFrame(predictions[0], columns=['open', 'high', 'low', 'close'], index=pred_index)
      
      # Combine historical data and predictions
      combined_df = pd.concat([historical_data, pred_df])
      
      # Create the plot
      fig = make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.03, subplot_titles=('EURUSD OHLC'))
      
      # Add historical candlestick
      fig.add_trace(go.Candlestick(x=historical_data.index,
                                   open=historical_data['open'],
                                   high=historical_data['high'],
                                   low=historical_data['low'],
                                   close=historical_data['close'],
                                   name='Historical'))
      
      # Add predicted candlestick
      fig.add_trace(go.Candlestick(x=pred_df.index,
                                   open=pred_df['open'],
                                   high=pred_df['high'],
                                   low=pred_df['low'],
                                   close=pred_df['close'],
                                   name='Predicted'))
      
      # Add a vertical line to separate historical data from predictions
      fig.add_vline(x=historical_data.index[-1], line_dash="dash", line_color="gray")
      
      # Update layout
      fig.update_layout(title='EURUSD OHLC Chart with Predictions',
                        yaxis_title='Price',
                        xaxis_rangeslider_visible=False)
      
      # Show the plot
      fig.show()
      
      # Print predictions (optional)
      print("Predicted prices for the next 24 hours:", predictions)
      


    Pythonによる訓練と予測コード

    Jupyterノートブックでコードベースを実行することに興味がない方向けに、添付ファイルで直接実行できるファイルをいくつか提供しています。 

    • model_training.py
    • model_prediction.py

    Jupyterを使用しなくても、思い通りにモデルを構成し、実行することができます。 


    予想結果

    モデルを訓練し、Pythonで予測コードを実行したところ、次のようなグラフが得られました。予測は2024年7月8日午前12時30分(CEST+3)頃に作成されました。これはちょうど日曜の夜/月曜の朝の開始です。EURUSDはギャップを持って始まったので、チャートにはギャップが見えます。このモデルは、EURUSDがこのギャップを埋める可能性のあるほとんどの期間、上昇トレンドを経験するはずであると予測しています。ギャップが埋まった後、値動きは終値付近で下降に転じるはずです。 

    Pythonからの予測

    また、結果の生の値も出力しました。 

    Predicted prices for the next 24 hours: [[[1.0789319 1.08056   1.0789403 1.0800443]
      [1.0791171 1.080738  1.0791024 1.0802013]
      [1.0792702 1.0807946 1.0792127 1.0802455]
      [1.0794896 1.0809869 1.07939   1.0804181]
      [1.0795166 1.0809793 1.0793561 1.0803629]
      [1.0796498 1.0810834 1.079427  1.0804263]
      [1.0798903 1.0813211 1.0795883 1.0805805]
      [1.0800778 1.081464  1.0796818 1.0806502]
      [1.0801392 1.0815498 1.0796598 1.0806476]
      [1.0802988 1.0817037 1.0797216 1.0807337]
      [1.080521  1.0819166 1.079835  1.08086  ]
      [1.0804708 1.0818571 1.079683  1.0807351]
      [1.0805807 1.0819991 1.079669  1.0807738]
      [1.0806456 1.0820425 1.0796478 1.0807805]
      [1.080733  1.0821087 1.0796758 1.0808226]
      [1.0807986 1.0822101 1.0796862 1.08086  ]
      [1.0808219 1.0821983 1.0796905 1.0808747]
      [1.0808604 1.082247  1.0797052 1.0808727]
      [1.0808146 1.082188  1.0796149 1.0807893]
      [1.0809066 1.0822624 1.0796828 1.0808471]
      [1.0809724 1.0822903 1.0797662 1.0808889]
      [1.0810378 1.0823163 1.0797914 1.0809084]
      [1.0810691 1.0823379 1.0798224 1.0809308]
      [1.0810966 1.0822875 1.0797993 1.0808865]]]


    事前訓練されたモデルをMQL5に持ち込む

    このセクションでは、予測されたプライスアクションをチャート上で可視化するのに役立つインディケ ータの前兆を作成します。読者の皆さんは、この複雑なニューラルネットワークをどのように使用するかについて、異なる目標や異なる戦略を持っているかもしれないので、意図的に初歩的でオープンエンドなスクリプトにしました。この指標はMQL5 EA形式で開発されています。以下がスクリプトの全文です。 

    //+------------------------------------------------------------------+
    //|                                            PatchTST Predictor    |
    //|                                                   Copyright 2024 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024"
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #resource "\\PatchTST\\patchtst_model.onnx" as uchar PatchTSTModel[]
    
    #define SEQ_LENGTH 168
    #define PRED_LENGTH 24
    #define INPUT_FEATURES 4
    
    long ModelHandle = INVALID_HANDLE;
    datetime ExtNextBar = 0;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
    {
       // Load the ONNX model
       ModelHandle = OnnxCreateFromBuffer(PatchTSTModel, ONNX_DEFAULT);
       if (ModelHandle == INVALID_HANDLE)
       {
          Print("Error creating ONNX model: ", GetLastError());
          return(INIT_FAILED);
       }
    
       // Set input shape
       const long input_shape[] = {1, SEQ_LENGTH, INPUT_FEATURES};
       if (!OnnxSetInputShape(ModelHandle, ONNX_DEFAULT, input_shape))
       {
          Print("Error setting input shape: ", GetLastError());
          return(INIT_FAILED);
       }
    
       // Set output shape
       const long output_shape[] = {1, PRED_LENGTH, INPUT_FEATURES};
       if (!OnnxSetOutputShape(ModelHandle, 0, output_shape))
       {
          Print("Error setting output shape: ", GetLastError());
          return(INIT_FAILED);
       }
    
       return(INIT_SUCCEEDED);
    }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
       if (ModelHandle != INVALID_HANDLE)
       {
          OnnxRelease(ModelHandle);
          ModelHandle = INVALID_HANDLE;
       }
    }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
    {
       if (TimeCurrent() < ExtNextBar)
          return;
    
       ExtNextBar = TimeCurrent();
       ExtNextBar -= ExtNextBar % PeriodSeconds();
       ExtNextBar += PeriodSeconds();
    
       // Prepare input data
       float input_data[];
       if (!PrepareInputData(input_data))
       {
          Print("Error preparing input data");
          return;
       }
    
       // Make prediction
       float predictions[];
       if (!MakePrediction(input_data, predictions))
       {
          Print("Error making prediction");
          return;
       }
    
       // Draw hypothetical future bars
       DrawFutureBars(predictions);
    }
    
    //+------------------------------------------------------------------+
    //| Prepare input data for the model                                 |
    //+------------------------------------------------------------------+
    bool PrepareInputData(float &input_data[])
    {
       MqlRates rates[];
       ArraySetAsSeries(rates, true);
       int copied = CopyRates(_Symbol, PERIOD_H1, 0, SEQ_LENGTH, rates);
       
       if (copied != SEQ_LENGTH)
       {
          Print("Failed to copy rates data. Copied: ", copied);
          return false;
       }
    
       ArrayResize(input_data, SEQ_LENGTH * INPUT_FEATURES);
       for (int i = 0; i < SEQ_LENGTH; i++)
       {
          input_data[i * INPUT_FEATURES + 0] = (float)rates[SEQ_LENGTH - 1 - i].open;
          input_data[i * INPUT_FEATURES + 1] = (float)rates[SEQ_LENGTH - 1 - i].high;
          input_data[i * INPUT_FEATURES + 2] = (float)rates[SEQ_LENGTH - 1 - i].low;
          input_data[i * INPUT_FEATURES + 3] = (float)rates[SEQ_LENGTH - 1 - i].close;
       }
    
       return true;
    }
    
    //+------------------------------------------------------------------+
    //| Make prediction using the ONNX model                             |
    //+------------------------------------------------------------------+
    bool MakePrediction(const float &input_data[], float &output_data[])
    {
       ArrayResize(output_data, PRED_LENGTH * INPUT_FEATURES);
    
       if (!OnnxRun(ModelHandle, ONNX_NO_CONVERSION, input_data, output_data))
       {
          Print("Error running ONNX model: ", GetLastError());
          return false;
       }
    
       return true;
    }
    
    //+------------------------------------------------------------------+
    //| Draw hypothetical future bars                                    |
    //+------------------------------------------------------------------+
    void DrawFutureBars(const float &predictions[])
    {
       datetime current_time = TimeCurrent();
       for (int i = 0; i < PRED_LENGTH; i++)
       {
          datetime bar_time = current_time + PeriodSeconds(PERIOD_H1) * (i + 1);
          double open = predictions[i * INPUT_FEATURES + 0];
          double high = predictions[i * INPUT_FEATURES + 1];
          double low = predictions[i * INPUT_FEATURES + 2];
          double close = predictions[i * INPUT_FEATURES + 3];
    
          string obj_name = "FutureBar_" + IntegerToString(i);
          ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, bar_time, low, bar_time + PeriodSeconds(PERIOD_H1), high);
          ObjectSetInteger(0, obj_name, OBJPROP_COLOR, close > open ? clrGreen : clrRed);
          ObjectSetInteger(0, obj_name, OBJPROP_FILL, true);
          ObjectSetInteger(0, obj_name, OBJPROP_BACK, true);
       }
    
       ChartRedraw();
    }

    上記のスクリプトを実行するには、以下の行の定義に注意してください。 

    #resource "\\PatchTST\\patchtst_model.onnx" as uchar PatchTSTModel[]

    つまり、EAフォルダの中にPatchTSTというサブフォルダを作成する必要があります。PatchTSTサブフォルダの中に、モデル訓練のONNXファイルを保存する必要があります。ただし、メインのEAはルートフォルダ自体に保存されます。 

    モデルの訓練に使用したパラメータも、スクリプトの先頭で定義されています。

    #define SEQ_LENGTH 168
    #define PRED_LENGTH 24
    #define INPUT_FEATURES 4

    このケースでは、168本前のバーを使用してONNXモデルに入力し、次の24本先のバーを予測します。open、high、low、closeという4つの入力機能があります。 

    また、OnTick()関数内の以下のコードにも注意してください。 

    if (TimeCurrent() < ExtNextBar)
       return;
    
    ExtNextBar = TimeCurrent();
    ExtNextBar -= ExtNextBar % PeriodSeconds();
    ExtNextBar += PeriodSeconds();

    ONNXモデルはコンピュータの処理能力を集中的に使用するので、このコードによって、新しい予測がバーごとに1回だけ生成されるようになります。この場合、1時間ごとのバーを使用しているので、予測は1時間に1回更新されます。 

    最後に、このコードではMQL5の描画機能を使用して先物バーを画面に描画します。 

    void DrawFutureBars(const float &predictions[])
    {
       datetime current_time = TimeCurrent();
       for (int i = 0; i < PRED_LENGTH; i++)
       {
          datetime bar_time = current_time + PeriodSeconds(PERIOD_H1) * (i + 1);
          double open = predictions[i * INPUT_FEATURES + 0];
          double high = predictions[i * INPUT_FEATURES + 1];
          double low = predictions[i * INPUT_FEATURES + 2];
          double close = predictions[i * INPUT_FEATURES + 3];
    
          string obj_name = "FutureBar_" + IntegerToString(i);
          ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, bar_time, low, bar_time + PeriodSeconds(PERIOD_H1), high);
          ObjectSetInteger(0, obj_name, OBJPROP_COLOR, close > open ? clrGreen : clrRed);
          ObjectSetInteger(0, obj_name, OBJPROP_FILL, true);
          ObjectSetInteger(0, obj_name, OBJPROP_BACK, true);
       }
    
       ChartRedraw();
    }

    MQL5でこのコードを実装し、モデルをコンパイルし、結果のEAをH1時間枠に配置すると、チャート上に今後追加されるいくつかのバーが表示されるはずです。私の場合、これは次のようになります。 


    新しく描画されたバーが右側に表示されない場合は、[Shift end of chart from right border]ボタンをクリックする必要があります。

    結論

    本稿では、2023年に紹介されたPatchTSTモデルの訓練を段階的におこないました。PatchTSTアルゴリズムがどのように機能するか、一般的な感覚を得ました。ベースコードには、ONNX変換に関連するいくつかの問題がありました。具体的には、Unfold演算子がサポートされていないことです。この問題を解決し、よりONNXに適したコードにしました。また、データの収集、モデルの訓練、今後24時間の予測など、モデルの基本に焦点を当てることで、記事の目的をトレーダー向けにしました。そして、MQL5で予測を実装し、お気に入りの指標やEAで完全に訓練されたモデルを使用できるようにしました。添付のZIPファイルで、私のコードをMQLコミュニティと共有できることを嬉しく思います。ご質問やご意見がありましたら、お知らせください。 


    MetaQuotes Ltdにより英語から翻訳されました。
    元の記事: https://www.mql5.com/en/articles/15198

    添付されたファイル |
    PatchTST.zip (4876.35 KB)
    データサイエンスと機械学習(第25回):回帰型ニューラルネットワーク(RNN)を用いたFX時系列予測 データサイエンスと機械学習(第25回):回帰型ニューラルネットワーク(RNN)を用いたFX時系列予測
    回帰型ニューラルネットワーク(Recurrent Neural Network: RNN)は、過去の情報を活用して将来の出来事を予測することに優れています。その驚くべき予測能力は、さまざまな領域で応用され、大きな成功を収めています。この記事では、外為市場のトレンドを予測するためにRNNモデルを導入し、外為取引における予測精度を高める可能性を示します。
    MQL5でインタラクティブなグラフィカルユーザーインターフェイスを作成する(第1回):パネルの製作 MQL5でインタラクティブなグラフィカルユーザーインターフェイスを作成する(第1回):パネルの製作
    この記事では、MetaQuotes Language 5 (MQL5)を使用して、グラフィカルユーザーインターフェイス(GUI)パネルを作成し、実装するための基本的な手順について説明します。カスタムユーティリティパネルは、一般的なタスクを簡素化し、重要な取引情報を可視化することで、取引におけるユーザーのインタラクションを向上させます。カスタムパネルを作成することで、トレーダーはワークフローを合理化し、取引操作の時間を節約することができます。
    エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
    この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
    母集団最適化アルゴリズム:クジラ最適化アルゴリズム(WOA) 母集団最適化アルゴリズム:クジラ最適化アルゴリズム(WOA)
    (WOA)は、ザトウクジラの行動と狩猟戦略に着想を得たメタヒューリスティクスアルゴリズムです。WOAの主なアイデアは、クジラが獲物の周囲に泡を作り、螺旋状の動きで獲物に襲いかかる、いわゆる「バブルネット」と呼ばれる捕食方法を模倣することです。