English Русский Español Deutsch 日本語 Português
preview
使用PatchTST机器学习算法预测未来24小时的价格走势

使用PatchTST机器学习算法预测未来24小时的价格走势

MetaTrader 5交易 | 14 一月 2025, 09:14
497 0
Shashank Rai
Shashank Rai

引言

当我开始深入研究与时间序列预测相关的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)跟我一起操作。我们将把步骤总结如下: 

    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,获取给定交易品种、时间框架和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)
    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进行训练所必需的。为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)
    6. 设置计算设备:如果可用,则将设备设置为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}")
    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. 为模型定义配置类:定义一个名为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()
    9. 初始化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)
    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格式。 

    模型已在PyTorch和ONNX格式中训练和保存。
    


    使用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根先前的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. 步骤 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. 步骤 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. 后处理和可视化: 我们对预测结果进行处理,创建一个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在大部分时间将呈上升趋势,可能会填补这个缺口。在缺口被填补后,价格走势应该在当天结束时转向下跌。 

    来自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智能交易系统(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

    附加的文件 |
    PatchTST.zip (4876.35 KB)
    开发回放系统(第 52 部分):事情变得复杂(四) 开发回放系统(第 52 部分):事情变得复杂(四)
    在本文中,我们将修改鼠标指针,以实现与控制指标的交互,确保可靠、稳定地运行。
    构建K线趋势约束模型(第五部分):通知系统(第三部分) 构建K线趋势约束模型(第五部分):通知系统(第三部分)
    本系列文章的这一部分专门介绍如何将WhatsApp与MetaTrader 5集成以实现通知功能。我们包含一张流程图以简化理解,并将讨论在集成过程中安全措施的重要性。指标的主要目的是通过自动化的简化分析过程,并且它们应包含通知方法,以便在满足特定条件时向用户发出警报。欲了解更多信息,请阅读本文。
    神经网络变得简单(第 86 部分):U-形变换器 神经网络变得简单(第 86 部分):U-形变换器
    我们继续研究时间序列预测算法。在本文中,我们将讨论另一种方法:U-形变换器。
    跨邻域搜索(ANS) 跨邻域搜索(ANS)
    本文揭示了跨邻域搜索(ANS)算法的潜力,作为重要的一步,旨在开发灵活且智能的优化方法,使其能够在搜索空间中考虑问题的具体特性和环境的动态变化。