
使用PatchTST机器学习算法预测未来24小时的价格走势
引言
当我开始深入研究与时间序列预测相关的AI进展时,我在Huggingface.co上首次接触到了一个名为PatchTST的算法。对于任何使用过大型语言模型(LLMs)的人来说,Transformer的发明无疑是自然语言、图像和视频处理工具开发领域的一场革命。但时间序列呢?它是不是被遗忘了?还是说大部分相关研究都只是秘而不宣?事实证明,有许多新的模型成功地将Transformer应用于时间序列预测。在本文中,我们将探讨其中的一种实现。
PatchTST令人印象深刻之处在于其训练模型的快速性以及使用训练好的模型与MQL的便捷性。我坦诚地说,我对神经网络的概念还很陌生。但是,通过这个过程,并解决了本文中为MQL5概述的PatchTST的实施问题,我感觉自己在学习和理解这些复杂神经网络的开发、故障排除、训练和使用的道路上迈出了巨大的一步。这就像把一个刚学会走路的孩子放到一支职业足球队里,期望他在世界杯决赛中踢进致胜一球一样。虽然挑战巨大,但正是这种挑战促使我不断学习和成长。
PatchTST 概览
在发现PatchTST之后,我开始查阅阐释其设计原理的论文:《时间序列的64个金词:基于Transformer的长期预测》。标题很有趣。当我开始更深入地阅读这篇论文时,我心想,哇,这看起来是一种迷人的结构——它包含了许多我一直想学习的元素。因此,我自然而然地想要尝试一下,看看它的预测效果如何。以下是这个算法让我更加感兴趣的原因:
- 你可以使用PatchTST预测开盘价、最高价、最低价和收盘价。使用PatchTST时,我感觉你可以将整个数据(包括开盘价、最高价、最低价、收盘价,甚至交易量)一股脑儿地输入进去。你可以期待它能从数据中找出规律,因为所有数据都被转换成了一种称为“补丁(patches)”的东西。关于补丁的更多信息,稍后在本文中会详细介绍。现在,你只需要知道补丁很有吸引力,并且有助于提升预测效果。
- PatchTST的最小数据预处理要求。当我开始进一步深入研究这个算法时,我意识到作者使用了一种称为“RevIn”的技术,即反向实例归一化。RevIn来源于一篇题为《针对分布偏移的准确时间序列预测的可逆实例归一化》的论文。RevIn试图解决时间序列预测中的分布偏移问题。作为算法交易者,我们都太熟悉这种感觉了:我们训练好的交易执行算法(EA)似乎不再能预测市场走势,我们不得不重新优化和更新参数。可以把RevIn看作是实现同样目标的一种方法。
- 这种方法基本上会接收传入的数据,并使用以下公式对其进行归一化处理:
x = (x - mean) / std
然后,当模型需要进行预测时,它会使用相反的属性来对数据进行反归一化处理:
x = x * std + mean
RevIn 的另一个属性affine_bias。简而言之,affine_bias 是一个可学习的参数,它负责处理数据集中可能存在的偏度、峰度等问题。
x = x * affine_weight + affine_bias
PatchTST 的结构可以概括为以下步骤:
输入数据 -> RevIn -> 序列分解 -> 趋势分量 -> PatchTST 主干 -> TSTiEncoder -> Flatten_Head -> 趋势预测器 -> 残差分量 -> 趋势和残差相加 -> 最终预测
我们了解到,数据将通过 MT5 进行拉取。我们也已经讨论了 RevIn 的工作原理。
PatchTST 的工作原理如下:假设你拉取了 80,000 条 EURUSD 数据的 H1 时间框架数据。这大约是 13 年的数据量。使用 PatchTST,你会将数据分割成所谓的“补丁”。作为类比,可以把补丁想象成 Vision Transformers (ViTs) 如何处理图像,但这里是针对时间序列数据进行了适应。例如,如果补丁长度为 16,则每个补丁将包含 16 个连续的价格值。这就像一次只看时间序列的一小部分,这有助于模型在考虑全局模式之前专注于局部模式。
接下来,补丁中包含位置编码以保留序列顺序,这有助于模型记住每个补丁在序列中的位置。
Transformer 将归一化和编码后的补丁通过一堆编码器层传递。每个编码器层包含一个多头注意力层和一个前馈层。多头注意力层允许模型关注输入序列的不同部分,而前馈层允许模型学习数据的复杂非线性变换。
最后,我们有趋势分量和残差分量。趋势分量和残差分量都应用了相同的补丁分割、归一化、位置编码和 Transformer 层。然后,我们将趋势分量和残差分量的输出相加,以产生最终预测。
PatchTST 官方代码库问题
PatchTST 的官方代码库可以在 GitHub 上通过以下链接找到:PatchTST(ICLR 2023)。有两种不同的版本可供选择——监督学习和无监督学习。对于本文,我们将使用监督学习方法。正如我们所知,为了使用任何模型与 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]
ONNX不支持Unfold函数的转换。你会得到一个像这样的报错:
Unsupported: ONNX export of operator Unfold, input size not accessible. 不要有顾虑,请在 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中使用它。
我专门解决了这个问题。解决这个问题花费了最长的时间。然后,我将所有内容整合到一个名为patchTST.py的文件中,该文件可以在本文附带的zip文件中找到。这就是我们将用于模型训练的文件。
在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,获取给定交易品种、时间框架和K线数量的数据,然后返回一个包含开盘价、最高价、最低价和收盘价列的数据框(DataFrame)。
# 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进行训练所必需的。为torch设置了一个手动种子,以确保结果的可重复性。
# 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(即使用GPU),否则使用CPU。这对于在训练过程中利用GPU加速至关重要,尤其是当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)
-
为模型定义配置类:定义一个名为Config的配置类,用于存储PatchTST模型所需的所有超参数和设置。
# 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模型,并将其移动到所选设备(GPU或CPU)上。
# 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格式。
模型已在PyTorch和ONNX格式中训练和保存。
使用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根先前的K线,因为这是我们的模型进行预测所需的数量。
# 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行来为模型准备输入数据。在拉取数据时,我们只需要最后168小时的1小时数据来预测接下来的24小时。这是因为我们是这样训练模型的。
# 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. 后处理和可视化: 我们对预测结果进行处理,创建一个DataFrame,并使用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日周日晚上/周一早上开盘时(中欧夏令时+3,即凌晨12:30左右)创建的。此时正是周日晚上/周一早上的开盘时间。我们可以在图表中看到一个缺口,因为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智能交易系统(Expert Advisor)的格式开发的。以下是完整的脚本:
//+------------------------------------------------------------------+ //| 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[]
这意味着在智能交易系统(Expert Advisor)文件夹内,我们需要创建一个名为PatchTST的子文件夹。在PatchTST子文件夹中,我们需要保存从模型训练中得到的ONNX文件。然而,主要的EA文件将存储在根文件夹中。
我们用于训练模型的参数也在脚本的顶部进行了定义:
#define SEQ_LENGTH 168 #define PRED_LENGTH 24 #define INPUT_FEATURES 4
在我们的案例中,我们想要使用前168根K线数据作为输入,将这些数据送入ONNX模型,并获取对未来24根K线的预测。我们有4个输入特征:开盘价、最高价、最低价和收盘价。
此外,请注意OnTick()函数中的以下代码:
if (TimeCurrent() < ExtNextBar) return; ExtNextBar = TimeCurrent(); ExtNextBar -= ExtNextBar % PeriodSeconds(); ExtNextBar += PeriodSeconds();
由于ONNX模型对计算机的处理能力要求较高,这段代码将确保每根K线只生成一次新的预测。在我们的案例中,因为我们使用的是小时线,所以预测将每小时更新一次。
最后,在这段代码中,我们将通过MQL5的绘图功能在屏幕上绘制未来的K线:
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(1小时)时间框架上之后,你应该会在你的图表上看到未来增加了一些额外的K线。在本例中,它看起来是这样的:
请注意,如果您在图表右侧没有看到新绘制的K线,您可能需要点击“将图表末端从右侧边界移开”的按钮。
结论
在本文中,我们采取了循序渐进的方法,来训练2023年推出的PatchTST模型。我们大致了解了PatchTST算法的工作原理。基础代码存在一些与ONNX转换相关的问题。具体来说,“Unfold”操作不受支持,因此我们解决了这个问题,使代码更加兼容ONNX。为了让文章内容对交易者更加友好,我们重点关注了模型的基础知识、数据的获取、模型的训练以及获取未来24小时的预测结果。然后,我们在MQL5中实现了这个预测功能,这样我们就可以将完全训练好的模型与我们喜爱的指标和专家顾问(EA)结合使用了。我很高兴将我的所有代码与MQL社区的成员们分享,代码已附在zip文件中。如有任何问题或意见,请随时告知我。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15198
