
PatchTST機械学習アルゴリズムによる24時間の値動きの予測
はじめに
私が初めて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を使用して私と一緒に追うことができます。以下にその手順をまとめます。
-
必要なライブラリのインポート: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
-
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)
-
スライディングウィンドウを使用して予測データを準備する:関数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)
-
データを訓練セットとテストセットに分ける:データを訓練用とテスト用に分け、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:]
-
データを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)
-
計算デバイスを設定する:利用可能であればデバイスを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}")
-
訓練データ用のデータローダーを作成する:訓練データのバッチ処理とシャッフルをおこなうデータローダーを作成します。
# Step 7: Create DataLoader for training data train_dataset = TensorDataset(X_train, y_train) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
-
モデルの構成クラスを定義する: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()
-
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)
-
オプティマイザと損失関数を定義する:モデルを訓練するためのオプティマイザ(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
-
モデルを訓練する:指定されたエポック数にわたってモデルを訓練します。各バッチのデータに対して、モデルはフォワードパスを実行し、損失を計算し、勾配を計算するためにバックワードパスを実行し、モデルパラメータを更新します。
# 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}")
-
モデルをPyTorch形式で保存する:訓練済みモデルの状態辞書をファイルに保存します。このファイルを使用してpythonで直接予測をおこなうことができます。
# Step 12: Save the model in PyTorch format torch.save(model.state_dict(), 'patchtst_model.pth')
-
ONNXエクスポート用のダミー入力を準備する:ONNX形式へのモデルのエクスポートに使用するダミーの入力テンソルを作成します。
# Step 13: Prepare a dummy input for ONNX export dummy_input = torch.randn(1, seq_length, 4).to(device)
-
モデルを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
結果は次のように可視化できます。
また、エラーや警告のない次のような出力が得られ、モデルがONNX形式に正常に変換されたことがわかります。
Model trained and saved in PyTorch and ONNX formats.
Pythonを使用したステップごとの予測生成
では、予測コードを見てみましょう。
- ステップ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 - 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 - 入力データの準備:最後の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 - 構成の定義:モデルのパラメータを設定するために構成クラスを定義します。これらの設定は、モデルの訓練に使用したものと同じです。
# 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 - 訓練済みモデルの読み込み:訓練済み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 - 予測をおこなう:読み込まれたモデルと入力データを使用して予測をおこなう関数を定義します。
# 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 - 後処理と可視化:予測を処理し、データフレームを作成し、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がこのギャップを埋める可能性のあるほとんどの期間、上昇トレンドを経験するはずであると予測しています。ギャップが埋まった後、値動きは終値付近で下降に転じるはずです。
また、結果の生の値も出力しました。
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





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
このモデルの予測結果が実際の状況とまったく一致しないことがよくあるんだ。このモデルのコードに変更は加えていません。ご指導いただけないでしょうか?ありがとうございます。
このモデルの経験を共有していただきありがとうございます。予測の一貫性についてのご指摘はもっともです。PatchTSTモデルは、複数の市場要因を考慮する包括的なトレーディング・アプローチに統合されたときに、最も効果的に機能します。以下は、モデルの予測をより効果的に使用する方法です:
その他の個人的見解
モデルの予測は、唯一の判断材料ではなく、分析の一要素として使うべきです。これらの要素を取り入れることで、PatchTSTモデルを使用する際の取引結果の一貫性を改善できる可能性があります。
フェア・バリュー・ギャップ(FVG)スクリプト(これらのギャップは、私の経験上、需給ゾーンのような働きをします:)
ご関心をお寄せいただきありがとうございます!しかし、M1データに切り替える際には、いくつか重要な考慮事項があります:
1.データ量:データ量:10080分(1週間)のM1データでトレーニングすることは、H1データよりもはるかに多くのデータポイントを扱うことを意味します。これは次のようなことを意味する:
2.モデル・アーキテクチャの調整:モデル・トレーニングのステップ8と予測コードのステップ4で、より大きな入力シーケンスに対応するために他のパラメーターを調整したくなるかもしれません:
3.予測の質:3.予測品質:より詳細な予測が得られる一方で、M1データには通常より多くのノイズが含まれていることに注意してください。最適なバランスを見つけるために、異なる配列長や予測ウィンドウを試してみるとよいでしょう。洞察に感謝する。私のコンピューターは、256GBで64物理コアと、それなりに高性能だ。もっといいGPUが必要だけどね。
GPUをアップデートしたら、アップデートしたコンフィグ設定を試してみるつもりだ。
このモデルの経験を共有してくれてありがとう。予測の一貫性についてのご指摘はもっともです。PatchTSTモデルは、複数の市場要因を考慮する包括的な取引手法に統合されたときに最も効果を発揮します。以下は、モデルの予測をより効果的に使用する方法です:
その他の個人的見解
モデルの予測は、唯一の判断材料ではなく、分析の一要素として使うべきです。これらの要素を取り入れることで、PatchTSTモデルを使用する際の取引結果の一貫性を改善できる可能性があります。
フェア・バリュー・ギャップ(FVG)スクリプト(これらのギャップは、私の経験上、需給ゾーンのような働きをします:)
辛抱強く答えてくださり、無私無欲の分かち合いをありがとうございました。これほど詳細でプロフェッショナルな回答は初めてです。あなたの記事を繰り返し読みます。これらの知識は私にとって特に貴重です。よろしくお願いします。
ありがとうございます。ありがとうございます!また何かありましたらご連絡ください!