English Русский Español Português
preview
交易中的神经网络:针对加密货币市场的记忆扩充上下文感知学习(终篇)

交易中的神经网络:针对加密货币市场的记忆扩充上下文感知学习(终篇)

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

概述

上一篇文章中,我们讲述了MacroHFT 框架,其为高频加密货币交易(HFT)而开发。该框架体现了一种新潮方式,结合了上下文依赖的强化学习方法与记忆利用,能高效适应动态市场条件,同时把风险最小化。

MacroHFT 的操作原理基于其各个组件的两阶段训练。第一阶段,市场状态根据趋势方向和波动率水平进行分类。这一过程能够识别关键市场状态,随后用来训练专业的子智代。每个子智代都经过优化,从而适应特定场景。在第二阶段,训练搭配记忆模块的超智代来协调子智代的工作。该模块会参考历史数据,基于既往经验做出更精确的决策。

MacroHFT 架构包含若干关键组件。第一个是数据预处理模块,其负责过滤和归一化收到的市场信息。这剔除了噪声、并提升了数据品质,这对后续分析至关重要。

子智代是在特定市场场景上训练的深度学习模型。它们采用强化学习方法来适应复杂且快速变化的条件。最后一个组件是记忆扩充的超智代。它整合了子智代的输出,一并分析历史事件和当前市场状态。这样就能以高度预测准确性和韧性对抗市场的骤起骤落。

整合所有这些组件,令 MacroHFT 不仅能在高度波动的市场条件中有效运作,还显著提升了盈利能力量值。 

MacroHFT 框架的原始可视化如下所示。


在上一篇文章的实施章节,我们创建了一个超智代对象,并实现了其与子智代交互的算法。今天,我们将继续这项工作,专注于 MacroHFT 架构的新层面。


风险管理模块

在前一篇文章中,我们组织超智代的操作作为 CNeuronMacroHFTHyperAgent 对象,并开发了其与子智代交互的算法。此外,我们决定采用之前创建的配备更复杂架构的分析智代作为子智代。初看,这看似足以实现 MacroHFT 框架。然而,当前实现有某些局限性:子智代和超智代都仅分析环境状态。这虽可预测未来价格走势、判定交易方向、以及设置止损和止盈价位,但其并未涉及交易规模,而这是整体策略中至关重要的元素。

简单地使用固定交易规模,或基于相对于预期止损和账户余额的固定风险水平来计算成交量是可能的。然而,每个预测本身都带有独特的置信水平。逻辑推断,这一置信水平应在决定交易规模中扮演主角。预测高置信度意味着大笔交易,最大化整体盈利能力;而低置信度则建议更保守方式。

参考这些因素,我决定配备风险管理模块来强化实现。该模块将整合进现有架构,提供灵活、且自适应的交易规模调整方式。引入风险管理将提升模型应对不稳定市场条件的韧性,这在高频交易中尤为重要。

重点要注意,在这种情况下,风险管理算法部分“脱离”了直接环境分析。取而代之,专注于评估智代行为对财务结果的影响。其思路是将每笔交易与账户余额变化相关联,并识别指示政策有效的形态。可盈利交易数量的增长,结合余额稳步提升,示意当前政策的成功,证明每笔交易风险更高是合理的。相较之,亏损交易增加则表明需要采取更保守的策略,来降低风险。该方式不仅提升了对市场条件变化的适应性,还强化了整体资本管理效率。此外,为提升分析品质,将创建若干个账户状态预测,每个预测代表其当前和历史条件的不同层面。这能更精准地评估策略绩效,并启用对市场动态的及时适应。

风险管理算法已在 CNeuronMacroHFTvsRiskManager 对象中实现,其结构如下所示。

class CNeuronMacroHFTvsRiskManager  :  public CResidualConv
  {
protected:
   CNeuronBaseOCL       caAccountProjection[2];
   CNeuronMemoryDistil  cMemoryAccount;
   CNeuronMemoryDistil  cMemoryAction;
   CNeuronRelativeCrossAttention cCrossAttention;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
                       CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;

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

在所展现的结构中,能观察到一套标准的可覆盖方法、及若干内部对象,其在上述风险管理机制的实现中扮演者关键角色。在讲述类方法时,将详细讨论这些内部对象的功能,提供更深度理解它们的用法逻辑。

在我们的风险管理类中,所有内部对象都被声明为静态对象,简化了对象结构。这令构造和析构函数可留空,在于初始化或内存清理不需要额外的操作。所有继承和声明对象的初始化均在 Init 方法中执行,负责在创建类时建立类的架构。

类参数包括常量,是为无歧义的解释对象架构。

bool CNeuronMacroHFTvsRiskManager::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                        uint window, uint window_key, uint units_count, uint heads,
                                        uint stack_size, uint nactions, uint account_decr,
                                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CResidualConv::Init(numOutputs, myIndex, open_cl, 3, 3, (nactions + 2) / 3, optimization_type, batch))
      return false;

在方法主体中,会立即调用父类的同名方法。在这种情况下,它是一个带反馈的卷积模块。重点要注意,该模块的期望输出是一个张量,代表交易决策矩阵。每行描述一笔单独交易,并包含一个交易参数向量:成交量、止损和止盈。为了正确地组织交易分析,买卖交易被分开处理,允许对每笔交易进行独立分析。

在组织卷积运算时,内核大小和步长设置为 3,与交易描述中的参数数量对应。

接下来,我们来查看内部对象的初始化过程。重点要注意,风险管理模块依赖两个关键数据源:智代动作,以及描述所分析账户状态的向量。主数据流,代表智代动作,供为神经层对象。次级流包含账户状态描述,经由数据缓冲区传递。

为了所有内部组件的正常工作,两个数据流都必须以神经层对象表示。因此,第一步是初始化一个全连接神经层,来自第二个流的数据会进入该层。

   int index = 0;
   if(!caAccountProjection[0].Init(0, index, OpenCL, account_decr, optimization, iBatch))
      return false;

下一阶段加入了一个全连接层,设计用来形成账户状态描述的投影。该可训练层生成一个张量,包含指定维度子空间中所分析账户状态的多个投影。投影数量和子空间的维度均作为方法参数提供,允许针对不同任务灵活进行层配置。

   index++;
   if(!caAccountProjection[1].Init(0, index, OpenCL, window * units_count, optimization, iBatch))
      return false;

由风险管理模块接收的原产数据,仅提供所分析状态的静态描述。然而,为了准确评估智代的政策有效性,必须参考动态变化。记忆模块会应用在两条信息流,捕捉数据的时态序列。一个关键决策是存储原始账户状态向量、亦或其投影。原始向量更小、且资源更高效,而生成的投影记忆处理后,会把账户余额动态纳入静态数据,提供更丰富的信息。

   index++;
   if(!cMemoryAccount.Init(caAccountProjection[1].Neurons(), index, OpenCL, account_decr,
                           window_key, 1, heads, stack_size, optimization, iBatch))
      return false;

智代交易的记忆模块是在独立交易级别操作。

   index++;
   if(!cMemoryAction.Init(0, index, OpenCL, 3, window_key, (nactions + 2) / 3,
                          heads, stack_size, optimization, iBatch))
      return false;

为了更有效地分析政策,采用了交叉注意力模块。该模块将近期智代动作与账户状态动态关联,识别决策与财务结果之间的关系。

   index++;
   if(!cCrossAttention.Init(0, index, OpenCL, 3, window_key, (nactions + 2) / 3,
                            heads, window, units_count, optimization, iBatch))
      return false;
//---
   return true;
  }

此刻,内部对象的初始化完成。整个方法也在于此结束。我们只需将操作的逻辑结果返回至调用程序。

风险管理对象初始化之后,我们继续在 feedForward 方法中构造前馈通验算法。

bool CNeuronMacroHFTvsRiskManager::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(caAccountProjection[0].getOutput() != SecondInput)
     {
      if(!caAccountProjection[0].SetOutput(SecondInput, true))
         return false;
     }

该方法接收两个指向原始数据对象的指针。一个作为数据缓冲区提供,其内容必须传送到一个内部神经层对象。取代复制所有数据,我转而采用了更高效的方法:将内部对象的缓冲区指针替换为输入数据缓冲区的指针。处理显著加速。

接下来,这两条信息流都因加入了累积动态的额外数据而越发丰富。数据经由专门的记忆模块传递,这些模块捕捉过去的状态和变化,能够保留时态依赖关系,并维护上下文,以便更准确地处理。

   if(!cMemoryAccount.FeedForward(caAccountProjection[0].AsObject()))
      return false;
   if(!cMemoryAction.FeedForward(NeuronOCL))
      return false;

基于这些丰富的数据,生成账户状态向量的投影。这些投影提供了全面基础,可分析账户动态、并评估过去动作对当前状态的影响。

   if(!caAccountProjection[1].FeedForward(cMemoryAccount.AsObject()))
      return false;

一旦初步数据处理阶段结束,智代的政策对财务成果的影响将经由交叉注意力模块进行分析。将智代动作与财务变化相关联,揭示了决策与成果之间的关系。

   if(!cCrossAttention.FeedForward(cMemoryAction.AsObject(), caAccountProjection[1].getOutput()))
      return false;

形成交易决策的最后“润色”由父类机制提供,其执行最终的信息处理。

   return CResidualConv::feedForward(cCrossAttention.AsObject());
  }

这些操作的逻辑结果会返回至调用程序,并结束方法。

反向传播方法采用线性算法,在独立研究期间不太需要额外解释。至此,风险管理对象的复查完毕。该类的完整代码、及其所有方法都包含在附件之中。


模型架构

我们继续实现 MacroHFT 框架方式的 MQL5 版本。下一阶段涉及构建可训练模型的架构。在这种情况下,我们将训练一个单一参与者模型,其架构在 CreateDescriptions 方法中定义。

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

该方法接收指向动态数组对象的指针,记录所创建模型的架构。在方法主体中,我们立即检查所接收指针的相关性。如有必要,我们会创建一个动态数组对象的新实例。

接下来,我们创建一个全连通层的描述,在此情况下用于接收原始输入数据,且必须有足够大小,以便容纳描述所分析环境状态的张量。

//--- 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;
     }

归一化之后,环境状态描述被传递至我们在 MacroHFT 框架内创建的层。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMacroHFT;
//--- Windows
     {
      int temp[] = {BarDescr, 120, NActions}; //Window, Stack Size, N Actions
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.count = HistoryBars;
   descr.window_out = 32;
   descr.step = 4;                              // Heads
   descr.layers =3;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

注意,MacroHFT 设计在 1-分钟时间帧上操作。相应地,环境状态的记忆堆栈提升到 120 个元素,对应一条 2-小时的序列。这样就能更全面地审计市场动态,从而在交易策略内实现更准确的预测、及决策制定。

如前所述,该模块专注于环境状态分析,并未提供风险评估能力。因此,下一步是加入风险管理模块。

//--- layer 3
   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;
     }

在这种情况下,我们将记忆堆栈降至 15 个元素,减少所处理的数据量,并允许专注于短期动态。这确保了对市场变化的更快反应。

风险管理模块的输出是归一化值。为了将它们映射到智代所需的动作空间,我们使用含有相应激活函数的卷积层。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvSAMOCL;
   descr.count = NActions / 3;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 3;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability = Rho;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

方法完成后,会返回操作的逻辑结果至调用程序。

注意,在该实现中,我们并未针对智代采用随机头。以我视角,在高频交易中,用它只会引入不必要的噪音。在高频交易策略中,尽量减少随机因素至关重要,以此确保对市场变化的快速、且依据充分的反射。


训练模型

在该阶段,我们已按自己的解释,完成了 MacroHFT 框架作者所提议方式的 MQL5 版本实现。可训练模型的架构已有定义。现在是时候去训练模型了。不过,首先我们需要收集一个训练数据集。此前,模型是用小时时间帧数据进行训练的。在这种情况下,我们需要的信息来自 1-分钟时间帧。

重点要注意,降低时间帧会增加数据体量。显然,同一历史间隔产生了 60 倍的柱线。如果其它参数保持不变,这就导致训练数据集大小会按比例增加。因此必须采取措施来降低它。这有两种方式:缩短训练区间、或减少训练数据集中存储的通验次数。

我们决定维持一年的训练区间,以我观点,其是至少能洞察季节性的最小间隔。不过每次通验时间限制为一个月。每月保存两次随机政策通验,总计 24 个通验。而这对于完整训练尚有不足,但这种格式已能生成超过 3 GB 的训练数据集文件。

为了收集训练数据集,约束条件相当严格。应当注意的是,没有人指望随机智代政策能带来可盈利的结果。毫不意外,我们在所有通验期间快速损失了全部本金。为了预防测试因保证金催缴而中断,我们设定了一个生成交易决策的最小账户余额阈值。这令我们能够在数据集中保留所分析期间的所有环境状态,尽管交易并无奖励。

值得一提的是,MacroHFT 作者在训练加密货币交易模型时用到自己的技术指标列表。该列表可在原文附录中找到。

我们选择维持之前所用的分析指标列表。这样就能直接比较所实现方案、与之前构建和训练模型的有效性。使用相同的指标来确保客观评估,直接比较结果则可识别新模型的优缺点。

收集训练数据集的数据则由智能系统 “...\MacroHFT\Research.mq5” 执行。至于本文,我们将专注于 OnTick 方法,其中实现了获取终端数据和执行交易的核心算法。

void OnTick()
  {
//---
   if(!IsNewBar())
      return;

在方法主体中,我们首先检查新柱线开立。仅在这之后,才会执行后续操作。起先,我们更新所分析技术指标数据,并加载价格走势的历史数据。

   int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates);
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();

接下来,我们组织一个环路,基于收自终端的数据,形成描述环境状态的缓冲区。

   float atr = 0;
   for(int b = 0; b < (int)HistoryBars; b++)
     {
      float open = (float)Rates[b].open;
      float rsi = (float)RSI.Main(b);
      float cci = (float)CCI.Main(b);
      atr = (float)ATR.Main(b);
      float macd = (float)MACD.Main(b);
      float sign = (float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
                               macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         continue;
      //---
      int shift = b * BarDescr;
      sState.state[shift] = (float)(Rates[b].close - open);
      sState.state[shift + 1] = (float)(Rates[b].high - open);
      sState.state[shift + 2] = (float)(Rates[b].low - open);
      sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f);
      sState.state[shift + 4] = rsi;
      sState.state[shift + 5] = cci;
      sState.state[shift + 6] = atr;
      sState.state[shift + 7] = macd;
      sState.state[shift + 8] = sign;
     }
   bState.AssignArray(sState.state);

应当注意的是,振荡器数值外观相似,且能随时间维持分布稳定性。为达成这一点,仅需分析价格走势指标之间的偏差,预留分布稳定性,避免过度波动对分析结果造成的失真。

下一步是创建账户状态描述向量,参考持仓和达成的财务成果。首先,我们收集持仓信息。

   sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY);
//---
   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   double position_discount = 0;
   double multiplyer = 1.0 / (60.0 * 60.0 * 10.0);
   int total = PositionsTotal();
   datetime current = TimeCurrent();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * 
                                                      multiplyer * MathAbs(profit);
     }
   sState.account[2] = (float)buy_value;
   sState.account[3] = (float)sell_value;
   sState.account[4] = (float)buy_profit;
   sState.account[5] = (float)sell_profit;
   sState.account[6] = (float)position_discount;
   sState.account[7] = (float)Rates[0].time;

然后我们生成时间戳的谐波。

   bTime.Clear();
   double time = (double)Rates[0].time;
   double x = time / (double)(D'2024.01.01' - D'2023.01.01');
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_MN1);
   bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_W1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_D1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   if(bTime.GetIndex() >= 0)
      bTime.BufferWrite();

仅在准备工作完成后,我们才会将所有财务成果合并到单一数据缓冲区。

   bAccount.Clear();
   bAccount.Add((float)((sState.account[0] - PrevBalance) / PrevBalance));
   bAccount.Add((float)(sState.account[1] / PrevBalance));
   bAccount.Add((float)((sState.account[1] - PrevEquity) / PrevEquity));
   bAccount.Add(sState.account[2]);
   bAccount.Add(sState.account[3]);
   bAccount.Add((float)(sState.account[4] / PrevBalance));
   bAccount.Add((float)(sState.account[5] / PrevBalance));
   bAccount.Add((float)(sState.account[6] / PrevBalance));
   bAccount.AddArray(GetPointer(bTime));
//---
   if(bAccount.GetIndex() >= 0)
      if(!bAccount.BufferWrite())
         return;

所有必要的原始数据准备好之后,我们会检查账户余额。如果足够,模型会执行前馈通验。

   double min_lot = Symb.LotsMin();
   double step_lot = Symb.LotsStep();
   double stops = MathMax(Symb.StopsLevel(), 1) * Symb.Point();
//---
   vector<float> temp;
   if(sState.account[0] > 50)
     {
      if(!Actor.feedForward((CBufferFloat*)GetPointer(bState), 1, false, GetPointer(bAccount)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         return;
        }
      Actor.getResults(temp);
      if(temp.Size() < NActions)
         temp = vector<float>::Zeros(NActions);
      //---
      for(int i = 0; i < NActions; i++)
        {
         float random = float(rand() / 32767.0 * 5 * min_lot - min_lot);
         temp[i] += random;
        }
     }
   else
      temp = vector<float>::Zeros(NActions);

为了提升环境探索,在生成的交易决策中加入少量噪声。在初始阶段,随机策略执行期间看似这并无必要,而当使用预训练政策更新训练数据集时则证明很实用。

如果账户余额达到下限,交易决策向量将被填入零,表示无交易。

接下来,我们按获得的交易决策向量行事。起初,逆操作的交易量被排除在外。

   PrevBalance = sState.account[0];
   PrevEquity = sState.account[1];
//---
   if(temp[0] >= temp[3])
     {
      temp[0] -= temp[3];
      temp[3] = 0;
     }
   else
     {
      temp[3] -= temp[0];
      temp[0] = 0;
     }

然后我们检查多头仓位的参数。如果交易决策未指定多头仓位,我们会检查之前的持仓、并平仓。

//--- buy control
   if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= stops || 
                             (temp[2] * MaxSL * Symb.Point()) <= stops)
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }

如果有必要多头开仓或持仓,我们首先将交易参数调整为所需格式,并调整持仓的交易价位。

   else
     {
      double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot;
      double buy_tp = NormalizeDouble(Symb.Ask() + temp[1] * MaxTP * Symb.Point(), Symb.Digits());
      double buy_sl = NormalizeDouble(Symb.Ask() - temp[2] * MaxSL * Symb.Point(), Symb.Digits());
      if(buy_value > 0)
         TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);

然后我们通过缩放或部分平仓来调整持仓量。

      if(buy_value != buy_lot)
        {
         if(buy_value > buy_lot)
            ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
         else
            Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp);
        }
     }

空头持仓的参数处理方式类似。

//--- sell control
   if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= stops || 
                              (temp[5] * MaxSL * Symb.Point()) <= stops)
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
   else
     {
      double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;;
      double sell_tp = NormalizeDouble(Symb.Bid() - temp[4] * MaxTP * Symb.Point(), Symb.Digits());
      double sell_sl = NormalizeDouble(Symb.Bid() + temp[5] * MaxSL * Symb.Point(), Symb.Digits());
      if(sell_value > 0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
        {
         if(sell_value > sell_lot)
            ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
         else
            Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);
        }
     }

交易执行完毕后,会生成奖励向量。

   sState.rewards[0] = bAccount[0];
   sState.rewards[1] = 1.0f - bAccount[1];
   if((buy_value + sell_value) == 0)
      sState.rewards[2] -= (float)(atr / PrevBalance);
   else
      sState.rewards[2] = 0;

所有累积的数据随后被传送到训练数据集的数据存储缓冲区,等待新柱线开立事件。

   for(ulong i = 0; i < NActions; i++)
      sState.action[i] = temp[i];
   if(!Base.Add(sState))
      ExpertRemove();
  }

注意,如果无法往训练数据集缓冲区加入新数据,程序会被初始化并关闭。由于出错或缓冲区完全填满时,这就可能发生。

该智能系统的完整代码已在附件中提供。

训练数据集的实际收集是在 MetaTrader 5 策略测试器中通过慢速优化完成的。  

显然,靠有限通验次数收集的训练数据集需要一种特殊的方式来建模训练。尤其是考虑到大量数据仅包含环境状态信息,这限制了学习潜力。在这样条件下,基于“近乎理想”的交易决策来训练模型看似是最优的。这种方法,我们在训练若干个近期模型时曾经用过,尽管数据规模有限,但能竭力高效利用数据。

还值得注意的是,模型训练程序独用训练数据集,不依赖时间帧或财务工具收集所需数据。这提供了显著优势,这就能在不修改算法的情况下复用之前开发的训练程序。因此,现有资源和方法能被高效利用,节省时间和精力,同时不影响模型训练品质。


测试

我们已完成大量工作,按照我们的诠释,实现了 MacroHFT 框架作者提议方式的 MQL5 版本。下一步是评估已实现方法在真实历史数据上的有效性。

应当注意的是,此处呈现的实现与原版有显著差异,包括技术指标的选择。这必然会影响结果,故任何结论都是初步、且针对这些具体修改。

至于模型训练,我们采用了 EURUSD 的 2024 年 1-分钟时间帧(M1)的数据。分析指标参数保持不变,专注于评估算法和方法本身,避免指标设置的干扰影响。收集训练数据集和训练模型的流程如上所述。

训练好的模型在 2025 年 1 月的历史数据上进行了测试。测试结果呈现如下。

测试结果

应当注意的是,在为期两周的测试期间,该模型仅执行了 8 笔交易,对于高频交易的智能系统而言,这一数字无疑偏低。另一方面,执行交易的效率值得注意 — 仅有一笔非盈利交易。盈利因子为 2.47。

测试结果

通过详细分析交易历史,能够观察到在上涨趋势时的增长势头。


结束语

我们探讨了 MacroHFT 框架,这是一种创新且充满前景的加密货币市场高频交易工具。该框架的一个关键特点是能够同时参考宏观经济背景、及本地市场动态。这种组合令它们能够有效适应快速变化的金融环境,及更明智的交易决策。

在实际工作中,我们遵照拟议方式的解释,实现了 MQL5 版本,并对框架的运作做了一些调整。我们在真实历史数据上训练模型,并在训练集外的数据上测试。虽然执行的成交数量令人失望,且并未反映出典型的高频交易。这或许归因于技术指标的选择未做优化,或训练数据集有限。验证这些假设需要进一步调查。然而,测试结果展现出模型具备识别真正稳定形态的能力,在测试数据集中亦有较高比例的盈利交易成果。


参考


文章中所用程序

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

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

附加的文件 |
MQL5.zip (2376.99 KB)
开发多币种 EA 交易(第 22 部分):开始向设置的热插拔过渡 开发多币种 EA 交易(第 22 部分):开始向设置的热插拔过渡
如果要自动进行周期性优化,我们需要考虑自动更新交易账户上已经运行的 EA 设置。这样一来,我们就可以在策略测试器中运行 EA,并在单次运行中更改其设置。
交易中的神经网络:针对加密货币市场的记忆扩充上下文感知学习(MacroHFT) 交易中的神经网络:针对加密货币市场的记忆扩充上下文感知学习(MacroHFT)
我邀请您探索 MacroHFT 框架,该框架应用了上下文感知强化学习和记忆,利用宏观经济数据和自适应智代改进加密货币高频交易决策。
开发多币种 EA 交易(第 23 部分):整理自动项目优化阶段的输送机(二) 开发多币种 EA 交易(第 23 部分):整理自动项目优化阶段的输送机(二)
我们的目标是创建一个系统,用于自动定期优化最终 EA 中使用的交易策略。随着系统的发展,它变得越来越复杂,因此有必要不时地将其视为一个整体,以确定瓶颈和次优解决方案。
交易中的神经网络:配备概念强化的多智代系统(终篇) 交易中的神经网络:配备概念强化的多智代系统(终篇)
我们继续实现 FinCon 框架作者提议的方式。FinCon 是一款基于大语言模型(LLM)的多智代系统。今天,我们将实现必要的模块,并在真实历史数据上全面测试模型。