English Русский Español Deutsch 日本語
preview
神经网络在交易中的应用:将混沌理论融入时间序列预测(终篇)

神经网络在交易中的应用:将混沌理论融入时间序列预测(终篇)

MetaTrader 5交易系统 |
48 0
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

我们继续根据自己对 Attraos 框架作者提出的方法的理解来进行开发。在上一篇文章中,我们探讨了该框架的理论方面。该框架运用混沌理论原理来解决时间序列预测问题。

Attraos 框架的架构是一个复杂的、由多个组件组成的系统,融合了非线性分析、机器学习和计算优化的方法。利用相空间重构(PSR)方法,Attraos 能够模拟隐藏的动态过程,并考虑各种市场变量之间的非线性关系。这使得我们能够识别市场数据中的稳定结构,并利用这些结构来提高未来价格走势预测的准确性。

Attraos 的一个关键特性是多分辨率动态记忆单元(MDMU),它使模型能够保留历史价格走势模式并适应不断变化的市场条件。这在金融市场中尤为重要,因为市场模式可能会在不同时间间隔内以不同的幅度和强度重现。该模型能够动态适应金融市场不断变化的结构,从而在多个时间范围内提供更准确的预测。

在频域中应用局部演化策略,能够在适应不断变化的市场条件的同时,增强吸引子之间的差异性。这有助于模型最小化误差并控制吸引子偏差,从而确保稳定性和高预测准确性。

下面提供了 Attraos 框架的原始可视化图。

在上一篇文章的实践部分,我们实现了 OpenCL 端的基本组件。今天,我们将继续学习如何在主程序中创建对象。


构建 Attraos 对象

Attraos 算法从 PSR 模块开始,该模块根据指定的时间滞后将分析的时间序列转换为相空间。这一过程是数据预处理的关键步骤,能够识别出隐藏的依赖关系、时间序列结构和潜在的动态模式。

多维时间序列通常表示为一个矩阵,其中每一行包含被分析系统在给定时间点 t 的参数。然而,在我们的例子中,数据存储在一维缓冲区中,矩阵表示纯粹是常规的。数据的组织方式是,将每个时间点描述系统状态的向量按顺序存储在缓冲区中。每个向量的大小由 window 参数决定。因此,要创建具有给定时间延迟的子序列,只需按比例增加 window 值,同时减少序列长度即可。因此,将时间序列转换为相空间不需要额外的计算资源,并且完全是通过模型架构设计来实现的。

框架的所有后续操作都在 CNeuronAttraos 对象内进行组织,其结构概述如下。

class CNeuronAttraos :  public CNeuronBaseOCL
  {
protected:
   CNeuronBaseOCL    cOne;
   CNeuronBaseOCL    cX_norm;
   CNeuronConvOCL    cA;
   CNeuronConvOCL    cX_proj;
   CNeuronBaseOCL    cDelta;
   CNeuronBaseOCL    cB;
   CNeuronBaseOCL    cC;
   CNeuronConvOCL    cD;
   CNeuronBaseOCL    cH;
   CNeuronConvOCL    cDelta_proj;
   CNeuronBaseOCL    cDeltaA;
   CNeuronBaseOCL    cDeltaB;
   CNeuronBaseOCL    cDeltaBX;
   CNeuronBaseOCL    cDeltaH;
   CNeuronBaseOCL    cHS;
   //---
   virtual bool      PScan(void);
   virtual bool      PScanCalcGradient(void);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAttraos(void) {};
                    ~CNeuronAttraos(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronAttraos; }
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
  };

在这个新的类结构中,除了标准的可重写虚拟方法集外,我们还观察到了大量内部对象。它们执行不同的功能,并促进类元素之间的交互。使用内部对象可以更高效地组织代码。每个对象负责一项特定任务,使系统模块化且更易于修改。在实现该对象的方法时,我们将逐一分析各内部组件的功能,以说明它们在整体结构中的作用。

所有内部对象都是静态分配的,从而消除了动态创建和删除的需要。因此,类构造函数和析构函数保持为空,因为这些对象的内存管理是自动的。这些已声明和已继承对象的初始化是在 Init 方法中执行的。该方法接收常量作为参数,以唯一地定义正在创建的对象的架构。这些参数的结构应该一目了然。

bool CNeuronAttraos::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                       uint window, uint window_key, uint units_count,
                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      return false;
   SetActivationFunction(None);

在方法体中,首先调用父类中同名的方法。此方法设置最少的控制点并初始化继承的接口。

在此阶段,我们明确禁用了对象的激活函数,因为所有进程都是通过内部对象来处理的。继承接口仅用于全局级数据交换。

在父类方法成功执行后,我们继续初始化已声明的对象。首先,我们为两个可训练参数矩阵初始化对象:

  • A — 状态转移矩阵
  • D — 与原始数据残差连接的矩阵

由于这些矩阵将与包含所分析序列所有元素的完整矩阵相乘,因此它们的值必须在序列元素的数量上立即重复。这避免了额外的复制操作,并优化了反向传播过程。

和之前一样,我们使用一个小的两层模型来组织可训练参数。第一层包含固定值,第二层通过将内部可训练参数与第一层的固定值相乘来生成所需的张量。这种方法允许现有的神经层算法在不增加额外功能的情况下训练参数。为了减少第二层中的可训练参数数量,第一层通常只包含一个元素。

然而,在这种情况下,第二层的输出必须是一个具有重复值的张量。为了实现这一目标,第一层中的固定值会被重复指定次数。第二层被实现为一个卷积层,其滤波器数量等于可训练参数的数量。卷积窗口大小和步长设置为 1,以便每个输出张量元素都依赖于单个输入值。

   int index = 0;
   if(!cOne.Init(0, index, OpenCL, units_count, optimization, iBatch))
      return false;
   if(!cOne.getOutput().Fill(1))
      return false;
   cOne.SetActivationFunction(None);
//---
   index++;
   if(!cA.Init(0, index, OpenCL, 1, 1, window * window_key, units_count, 1, optimization, iBatch))
      return false;
   cA.SetActivationFunction(MinusSoftPlus);
   CBufferFloat *w = cA.GetWeightsConv();
   if(!w || !w.Fill(0))
      return false;

由于第一层不包含可训练参数,因此它也可以生成第二个可训练参数矩阵。因此,我们仅初始化第二个对象来生成可训练参数。

   index++;
   if(!cD.Init(0, index, OpenCL, 1, 1, window, units_count, 1, optimization, iBatch))
      return false;
   cD.SetActivationFunction(None);
   w = cD.GetWeightsConv();
   if(!w || !w.Fill(1))
      return false;

请注意,在对象初始化过程中,可训练参数矩阵会被填充为固定值。这与用随机值填充可训练参数的一般方法略有不同。当模型在早期训练阶段必须保持某些特性,或者初始条件对最终参数分布有强烈影响时,固定初始化非常有用。在这里,它能够防止初始的剧烈波动,并促进对数据的更平稳适应。

剩余的状态空间模型参数根据输入数据生成,从而使其能够适应所分析序列的特定特征。卷积层并行生成所有模型实体。这种方法确保了高效的数据处理,并显著加快了计算速度,因为卷积在整个序列中是并行执行的。

在生成状态空间模型参数之前,需要对输入进行归一化处理。归一化处理消除了原始值之间的尺度差异,使优化过程更加平滑且更具可预测性。

//---
   index++;
   if(!cX_norm.Init(0, index, OpenCL, window * units_count, optimization, iBatch))
      return false;
   cX_norm.SetActivationFunction(None);
   index++;
   if(!cX_proj.Init(0, index, OpenCL, window, window, 4 * window_key, units_count, 1, optimization, iBatch))
      return false;
   cX_proj.SetActivationFunction(None);

接下来,将生成的模型参数划分为各个独立的实体。此外,还会创建额外的对象用于存储,这些对象的名称反映了所存储的数据。

   index++;
   if(!cDelta.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cDelta.SetActivationFunction(None);
   index++;
   if(!cB.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cB.SetActivationFunction(None);
   index++;
   if(!cC.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cC.SetActivationFunction(None);
   index++;
   if(!cH.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cH.SetActivationFunction(None);

然后,我们初始化负责生成隐藏状态指数衰减参数的对象。这一组件对于管理序列中的信息动态至关重要,它控制着过去状态的保留或衰减程度。

   index++;
   if(!cDelta_proj.Init(0, index, OpenCL, window_key, window_key, window, units_count, 1, optimization, iBatch))
      return false;
   cDelta_proj.SetActivationFunction(SoftPlus);

使用 SoftPlus 作为激活函数可确保输出中只出现正值。

另外,还初始化了几个大小相同的其他对象,用于存储中间计算结果。

   index++;
   if(!cDeltaA.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaA.SetActivationFunction(None);
   index++;
   if(!cDeltaB.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaB.SetActivationFunction(None);
   index++;
   if(!cDeltaBX.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaBX.SetActivationFunction(None);
   index++;
   if(!cDeltaH.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaH.SetActivationFunction(None);
   index++;
   if(!cHS.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cHS.SetActivationFunction(None);
//---
   return true;
  }

初始化方法最后向调用程序返回一个逻辑结果。

请注意,在这个对象中,架构参数并未存储在单独的局部变量中。这种实现避免了维护已存储在内部对象中的值的持久副本。相反,局部变量是在前馈和反向传播方法开始时填充的。

对象初始化之后,我们继续执行前向传递算法,该算法在 feedForward 方法中实现,该方法接收指向输入数据对象的指针。

bool CNeuronAttraos::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//---
   uint window = cX_proj.GetWindow();
   uint window_key = cX_proj.GetFilters() / 4;
   uint units = cD.GetUnits();

在方法体中,我们首先从初始化过程中未存储的内部对象中加载参数。然后,我们为模型的可训练参数生成张量。

   if(!cA.FeedForward(cOne.AsObject()))   // (Units, Window, WindowKey)
      return false;
   if(!cD.FeedForward(cOne.AsObject()))   // (Units, Window))
      return false;

接下来,我们对输入数据进行归一化处理,并生成上下文相关的模型参数。

   if(!NeuronOCL ||
      !SumAndNormilize(NeuronOCL.getOutput(), NeuronOCL.getOutput(), cX_norm.getOutput(), window, true, 0, 0, 0, 0.5f))
      return false;
   if(!cX_proj.FeedForward(cX_norm.AsObject()))    // (Units, 4*WindowKey)
      return false;

随后将其拆分为独立的参数项。

   if(!DeConcat(cDelta.getOutput(), cB.getOutput(), cC.getOutput(), cH.getOutput(), cX_proj.getOutput(),
                window_key, window_key, window_key, window_key, units))   // 4*(Units, WindowKey)
      return false;

我们还生成自适应时间步长参数。

   if(!cDelta_proj.FeedForward(cDelta.AsObject()))       // (Units, Window)
      return false;

至此,准备阶段完成,我们继续构建 MDMU 算法,该算法负责对时间序列动态进行建模。模型状态根据循环方程进行更新:

其中 Δt 为自适应时间步长。

首先,我们计算第一项中的指数部分,用 SoftPlus 替换标准的指数函数,这样做有几个优点。

   if(!DiagMatMul(cDelta_proj.getOutput(), cA.getOutput(), cDeltaA.getOutput(),
                  window, window_key, units, SoftPlus))  // (Units, Window, WindowKey)
      return false;

SoftPlus 的增长速度慢于指数函数,从而降低了转移矩阵急剧增加的风险。这确保了梯度变化更加平滑,训练更加稳定。

指数函数对微小的 Δ 变化非常敏感。SoftPlus 可平滑变化,防止隐藏状态的突然跳变。

在嘈杂的数据中,SoftPlus 限制了异常值的影响,因为其增长受到对数约束,从而增强了模型的稳定性。

接下来,通过顺序矩阵乘法来计算第二项的值。

   if(!MatMul(cDelta_proj.getOutput(), cB.getOutput(), cDeltaB.getOutput(),
              window, 1, window_key, units))             // (Units, Window, WindowKey)
      return false;
   if(!DiagMatMul(cX_norm.getOutput(), cDeltaB.getOutput(), cDeltaBX.getOutput(),
                  window, window_key, units, None))      // (Units, Window, WindowKey)
      return false;

然后,我们根据隐藏状态的变化率来调整动态调节矩阵,以适应隐藏状态的变化。

   if(!MatMul(cDelta_proj.getOutput(), cH.getOutput(), cDeltaH.getOutput(),
              window, 1, window_key, units))             // (Units, Window, WindowKey)
      return false;

在准备好所有必要数据后,我们使用上一篇关于 OpenCL 方面的文章中实现的并行扫描算法来校正系统的隐藏状态。这里只需调用 PScan 内核包装器即可。

if(!PScan())
   return false;

内核调用遵循标准算法,因此我们在此不作详细讨论。该方法的完整代码包含在附件中(文件 NeuroNet.cl )。

接下来,我们通过将更新的隐藏状态矩阵乘以隐藏状态投影矩阵来生成分析系统的预测状态。

if(!MatMul(cHS.getOutput(), cC.getOutput(), Output, window, window_key, 1, units)) // (Units, Window, 1)
   return false;

归一化后的输入数据乘以直接连接系数。

if(!ElementMult(cD.getOutput(), cX_norm.getOutput(), PrevOutput))           // (Units, Window))
   return false;

然后将这两个运算的结果相加。

if(!SumAndNormilize(Output, PrevOutput, Output, window, false, 0, 0, 0, 1))   // (Units, Window))
   return false;

此外,我们通过创建残差连接路径来融合原始输入数据。

   if(!SumAndNormilize(Output, NeuronOCL.getOutput(), Output, window, false, 0, 0, 0, 1))   // (Units, Window))
      return false;
//---
   return true;
  }

这种方法整合了输入数据中隐藏状态和短期依赖关系的信息。

至此,我们 Attraos 框架实现的前向传递算法就完成了。然后,我们将操作的逻辑结果返回给调用者,并完成方法执行。

下一步是为我们的对象构建反向传播算法。在本文中,我们将探讨 calcInputGradients 方法,该方法用于分布误差梯度。与之前一样,该方法接收一个指向输入数据对象的指针,但这次它还会根据输入数据对最终模型输出的影响来获取误差大小。

bool CNeuronAttraos::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

该方法首先检查指针的有效性。如果指针无效或已过期,任何进一步的操作都将毫无意义。

然后,我们像在前向传播中一样,将输入数据参数保存在局部变量中。

uint window = cX_proj.GetWindow();
uint window_key = cX_proj.GetFilters() / 4;
uint units = cD.GetUnits();

接下来,我们将输出层的误差梯度分配到三个数据流中。让我提醒你,在前馈传递过程中,我们使用了3个数据传输信息流:

  • 状态空间模型
  • 带系数的直接连接
  • 残差连接

首先,误差梯度在直接连接系数和归一化输入数据之间进行分配。

if(!ElementMultGrad(cD.getOutput(), cD.getGradient(), cX_norm.getOutput(), cX_norm.getPrevOutput(),
                    Gradient, cD.Activation(), None))          // (Units, Window))
   return false;

接下来,梯度沿着第二个数据流传播,在隐藏状态和投影系数之间进行分配。

if(!MatMulGrad(cHS.getOutput(), cHS.getGradient(), cC.getOutput(), cC.getGradient(), Gradient,
               window, window_key, 1, units)) // (Units, Window, 1)
   return false;

如有必要,会使用相应激活函数的导数来对结果进行校正。

if(cHS.Activation() != None)
  {
   if(!DeActivation(cHS.getOutput(), cHS.getGradient(), cHS.getGradient(), cHS.Activation()))
      return false;
  }
if(cC.Activation() != None)
  {
   if(!DeActivation(cC.getOutput(), cC.getGradient(), cC.getGradient(), cC.Activation()))
      return false;
  }

然后,我们使用相应的内核包装器将梯度分布到并行扫描模块中。

if(!PScanCalcGradient())
   return false;

将所得值分配给相应的实体。首先,将梯度传递给隐藏状态和自适应时间步长参数。

if(!MatMulGrad(cDelta_proj.getOutput(), cDelta_proj.getGradient(), cH.getOutput(), cH.getGradient(),
               cDeltaH.getGradient(), window, 1, window_key, units))             // (Units, Window, WindowKey)
   return false;

然后,误差梯度被传播到归一化输入数据。

if(!DiagMatMulGrad(cX_norm.getOutput(), cX_norm.getGradient(), cDeltaB.getOutput(), cDeltaB.getGradient(),
                   cDeltaBX.getGradient(), window, window_key, units))      // (Units, Window, WindowKey)
   return false;
if(!SumAndNormilize(cX_norm.getGradient(), cX_norm.getPrevOutput(), cX_norm.getPrevOutput(),
                                                                  window, false, 0, 0, 0, 1))
   return false;

请注意,我们已经将误差梯度值传递到了归一化输入对象中。因此,在这个阶段,我们对来自两个信息流的数据进行了总结。

同样,梯度被分配给控制输入对隐藏状态影响的系数和自适应时间步长参数。

if(!MatMulGrad(cDelta_proj.getOutput(), cDelta_proj.getPrevOutput(), cB.getOutput(), cB.getGradient(),
               cDeltaB.getGradient(), window, 1, window_key, units))             // (Units, Window, WindowKey)
   return false;
if(!SumAndNormilize(cDelta_proj.getGradient(), cDelta_proj.getPrevOutput(), cDelta_proj.getGradient(),
                    window, false, 0, 0, 0, 1))
   return false;

自适应时间步参数的梯度会与之前收集的值进行累积。

接下来,将误差梯度传播到隐藏状态演化矩阵,并使用激活函数的导数来修正其值。

if(!DeActivation(cDeltaA.getOutput(), cDeltaA.getGradient(), cDeltaA.getGradient(), SoftPlus))
   return false;

然后,我们在各个实体之间分配这些值。

if(!DiagMatMulGrad(cDelta_proj.getOutput(), cDelta_proj.getPrevOutput(), cA.getOutput(), cA.getGradient(),
                   cDeltaA.getGradient(), window, window_key, units))  // (Units, Window, WindowKey)
   return false;
if(!SumAndNormilize(cDelta_proj.getGradient(), cDelta_proj.getPrevOutput(), cDelta_proj.getGradient(),
                    window, false, 0, 0, 0, 1))
   return false;

在此阶段,我们再次在自适应时间步长参数级别对误差梯度值进行求和。然而,这一次,这是从这个方向传来的最后一条信息流。然后,我们根据相应激活函数的导数来调整累积值。

if(cDelta_proj.Activation() != None)
  {
   if(!DeActivation(cDelta_proj.getOutput(), cDelta_proj.getGradient(), cDelta_proj.getGradient(),
                                                                        cDelta_proj.Activation()))
      return false;
  }

之后,我们将误差梯度传播到自适应时间步长的层面。

if(!cDelta.calcHiddenGradients(cDelta_proj.AsObject()))
   return false;

在此阶段,我们已经获得了所有上下文相关实体的误差梯度。这些值被收集到一个张量中。

if(!Concat(cDelta.getGradient(), cB.getGradient(), cC.getGradient(), cH.getGradient(), cX_proj.getGradient(),
           window_key, window_key, window_key, window_key, units))   // 4*(Units, WindowKey)
   return false;

然后,误差梯度被向下传播到归一化输入数据的层级。

if(!cX_norm.calcHiddenGradients(cX_proj.AsObject()))
   return false;
if(!SumAndNormilize(cX_norm.getGradient(), cX_norm.getPrevOutput(), cX_norm.getGradient(),
                                                               window, false, 0, 0, 0, 1))
   return false;

回想一下,归一化输入数据对象已经两次接收到误差梯度。因此,将此阶段获得的数值加到之前累积的梯度上。

在将累积梯度传递到输入数据层之前,我们还会结合残差连接路径中的值,并根据相应激活函数的导数对其进行调整。

   if(!SumAndNormilize(cX_norm.getGradient(), Gradient, cX_norm.getGradient(), window, false, 0, 0, 0, 1))
      return false;
   if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), cX_norm.getGradient(), NeuronOCL.Activation()))
      return false;
//---
   return true;
  }

至此,calcInputGradients 方法结束,该方法会向调用程序返回一个逻辑结果。

至于负责更新模型参数的 updateInputWeights 方法,我建议单独查看一下。它只是为包含可训练参数的四个内部对象调用相应的更新方法。

我想就保存和恢复对象状态的方法中的算法说几句。我们的新类包含大量内部对象,但其中只有四个包含可训练参数。因此,在保存时,只需将这四个对象记录到磁盘即可。

bool CNeuronAttraos::Save(const int file_handle)
  {
   if(!CNeuronBaseOCL::Save(file_handle))
      return false;
//---
   if(!cA.Save(file_handle))
      return false;
   if(!cD.Save(file_handle))
      return false;
   if(!cX_proj.Save(file_handle))
      return false;
   if(!cDelta_proj.Save(file_handle))
      return false;
//---
   return true;
  }

但问题在于如何恢复对象功能。在 Load 方法中,首先从磁盘读取先前保存的数据。

bool CNeuronAttraos::Load(const int file_handle)
  {
   if(!CNeuronBaseOCL::Load(file_handle))
      return false;
//---
   if(!LoadInsideLayer(file_handle, cA.AsObject()))
      return false;
   if(!LoadInsideLayer(file_handle, cD.AsObject()))
      return false;
   if(!LoadInsideLayer(file_handle, cX_proj.AsObject()))
      return false;
   if(!LoadInsideLayer(file_handle, cDelta_proj.AsObject()))
      return false;

然后将架构参数保存到本地变量中。

   uint window = cX_proj.GetWindow();
   uint window_key = cX_proj.GetFilters() / 4;
   uint units_count = cD.GetUnits();

剩余的算法反映了临时存储对象的初始化过程。

   if(!cOne.Init(0, 0, OpenCL, units_count, optimization, iBatch))
      return false;
   if(!cOne.getOutput().Fill(1))
      return false;
   cOne.SetActivationFunction(None);
   int index = 3;
   if(!cX_norm.Init(0, index, OpenCL, window * units_count, optimization, iBatch))
      return false;
   cX_norm.SetActivationFunction(None);
   index += 2;
   if(!cDelta.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cDelta.SetActivationFunction(None);
   index++;
   if(!cB.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cB.SetActivationFunction(None);
   index++;
   if(!cC.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cC.SetActivationFunction(None);
   index++;
   if(!cH.Init(0, index, OpenCL, window_key * units_count, optimization, iBatch))
      return false;
   cH.SetActivationFunction(None);
   index += 2;
   if(!cDeltaA.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaA.SetActivationFunction(None);
   index++;
   if(!cDeltaB.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaB.SetActivationFunction(None);
   index++;
   if(!cDeltaBX.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaBX.SetActivationFunction(None);
   index++;
   if(!cDeltaH.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cDeltaH.SetActivationFunction(None);
   index++;
   if(!cHS.Init(0, index, OpenCL, window * window_key * units_count, optimization, iBatch))
      return false;
   cHS.SetActivationFunction(None);
//---
   return true;
  }

这种方法优化了数据保存、对象恢复以及磁盘空间的使用。

至此,关于在 MQL5 中构建 Attraos 框架的讨论就结束了。CNeuronAttraos 类及其所有方法的完整代码包含在附件中。



模型架构

在实现了 Attraos 框架算法之后,让我们来描述可训练模型的架构。在本实验中,我们使用多任务学习训练了两个模型。架构在 CreateDescriptions 方法中定义,该方法接收指向两个动态数组的指针,模型架构描述存储在这两个动态数组中。

bool CreateDescriptions(CArrayObj *&actor, CArrayObj *&probability)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!probability)
     {
      probability = new CArrayObj();
      if(!probability)
         return false;
     }

在该方法中,我们首先检查指针的有效性,并在必要时创建新的对象实例。

所描述的第一个模型是 Actor 模型,该模型使用了之前实现的 Attraos 方法。和往常一样,该模型首先是一个全连接的输入层,然后是批量归一化层。这使得交易终端的原始数据可以直接输入到模型中。在这种情况下,它们的主要归一化是由模型内部完成的。 

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

接下来我们使用 Attraos 架构的第一层。它使用 5 步时间延迟将输入转换为相空间,相当于 1 分钟时间周期内的 5 分钟。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronAttraos;
   descr.window = BarDescr*5;    // 5 min
   descr.count = HistoryBars/5;  // 24
   descr.window_out = 256;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

第二层将延迟增加到 15 步。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronAttraos;
   descr.window = BarDescr*15;    // 15 min
   descr.count = HistoryBars/15;  // 8
   descr.window_out = 256;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

第三层则增加到 30 步。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronAttraos;
   descr.window = BarDescr*30;    // 30 min
   descr.count = HistoryBars/30;  // 4
   descr.window_out = 256;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

值得注意的是,每个 CNeuronAttraos 对象的输出与输入数据的维度相匹配。因此,下一个卷积层将张量的维度降低了三倍。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count=descr.count = HistoryBars/3;
   descr.window = BarDescr*3;
   descr.step = descr.window;
   int prev_window=descr.window_out = BarDescr;
   descr.activation = SoftPlus;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

接下来是决策层,由三个连续的全连接层组成。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 512;
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = TANH;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SoftPlus;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

所得输出已归一化。

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

然后添加风险管理模块。

//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMacroHFTvsRiskManager;
//--- Windows
     {
      int temp[] = {3, 15, NActions, AccountDescr}; //Window, Stack Size, N Actions, Account Description
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.count = 10;
   descr.window_out = 16;
   descr.step = 4;                              // Heads
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = NActions / 3;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 3;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

用于预测未来走势方向的概率模型完全继承自先前的工作,未做任何修改。此处省略其描述。附件中提供了可训练模型的完整架构描述,包括环境交互程序,这些程序也沿用了之前工作中的版本,未做任何更改。



测试

在两篇文章中,我们做了大量工作来调整和扩展 Attraos 框架的思想。我们现在已进入关键阶段:利用真实历史数据评估所实施方法的功能性和有效性。这一过程对于评估模型的实用性和其在不同市场条件下识别模式并产生稳定结果的能力至关重要。

该模型使用 2024 年全年的 EURUSD M1 历史数据进行训练。所有指标参数均保持默认值,未进行额外优化。这种方法消除了外部因素,如针对特定历史数据的参数调优,从而可以专注于模型的基本性能。使用不变的指标参数也能评估模型在无需持续干预或重新配置的情况下,适应真实市场动态的能力。

模型训练分两个阶段进行。第一阶段采用批量大小为 1,允许每次训练迭代从训练集中随机抽取一个完全随机状态。这使模型最大限度地接触到了新的状态。然而,仅凭这一点还不足以正确训练风险管理模块。因此,在第二阶段,批量大小增加到 60,使模型和风险管理模块能够在 60 个连续的环境状态中进行调整,这相当于在 1 分钟的时间尺度上调整一小时的情况。     

测试使用了 2025 年 1 月至 2 月的数据。选择这一时期是为了确保对之前未见的数据进行严格评估。所有其他实验参数均保持不变,以确保实验的可重复性和公平比较。这种方法消除了随机因素,能够客观评估算法性能。

测试结果如下所示。

在测试期间,该模型执行了 287 笔交易,其中近 39% 的交易盈利。尽管胜率相对较低,但由于盈亏比,该策略总体上仍取得了正收益。具体而言,每笔盈利交易的利润平均值是亏损平均值的两倍,从而弥补了不太成功的交易,并产生了总体正收益,利润系数为 1.15。

平均持仓时间超过 2 小时,表明策略倾向于做出短期和中期决策。值得注意的是,持仓时间最长的仓位持续了近两天。这一事实需要进一步分析。



结论

我们探索了 Attraos 框架,该框架利用混沌理论概念进行时间序列预测。该框架集成了非线性分析、相空间重构、多分辨率动态记忆和自适应算法。这些技术使得预测更加准确,交易模型更具适应性。

在实践部分,我们用 MQL5 实现了我们对这些方法的解释,在历史数据上构建和训练模型。对样本外数据的测试表明,该模型能够在未见过的数据上产生利润。然而,结果也揭示了一些问题。尤其值得注意的是,我们看到持仓时间延长。此外,余额曲线不够平滑。这些结果表明了其潜力,但也凸显了进一步优化的必要性。

需要注意的是,这些结论仅适用于本次实现。本文并未对 Attraos 的原始版本进行测试。


参考文献列表


本文中用到的程序

# 名称 类型 描述
1 Research.mq5 EA 样本采集 EA
2 ResearchRealORL.mq5
EA
使用 Real-ORL 方法采集样本的 EA
3 Study.mq5 EA 模型训练 EA
4 Test.mq5 EA 模型测试 EA
5 Trajectory.mqh 类库 系统状态和模型架构描述结构
6 NeuroNet.mqh 类库 用于创建神经网络的类库
7 NeuroNet.cl 代码库 OpenCL 程序代码

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/17371

附加的文件 |
MQL5.zip (2509.49 KB)
基于马尔可夫状态转移矩阵的神经网络自学习型EA 基于马尔可夫状态转移矩阵的神经网络自学习型EA
基于状态矩阵与神经网络的自训练智能交易系统(EA)我们将马尔可夫链与基于ALGLIB MQL5库开发的多层感知器(MLP)神经网络相结合。马尔可夫链与神经网络如何结合应用于外汇预测?
价格走势:数学模型与技术分析 价格走势:数学模型与技术分析
预测货币对走势是交易成功的重要因素。本文剖析各类价格运行模型,对比其优劣特性,并探究模型在交易策略中的实际落地方式。文中将介绍能够挖掘潜在行情规律、提升预测精准度的分析方法。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
您应该了解的MQL5向导技巧(第六十五部分):使用FrAMA与强力指数的交易形态 您应该了解的MQL5向导技巧(第六十五部分):使用FrAMA与强力指数的交易形态
分形自适应移动平均线(FrAMA)与强力指数震荡指标则是另一种可在MQL5智能交易系统(EA)中搭配使用的指标。这两个指标具备良好的互补性:FrAMA属于趋势跟踪指标,而强力指数是基于成交量的震荡指标。与之前一样,我们将借助MQL5向导快速挖掘这组指标的潜在交易价值。