您应当知道的 MQL5 向导技术(第 47 部分):配合时态差异的强化学习·综合运用
📘

您应当知道的 MQL5 向导技术(第 47 部分):配合时态差异的强化学习·综合运用

第 3/3 篇

「强化信号类的网络与Q表装配」

这段实现把交易决策拆成买/卖两套独立策略:CSignalTD 构造函数里先给 RL 结构定下 3 个动作(买、卖、观望)与 3 种环境状态(多头、空头、震荡),并分别 new 出 QL_BUY 与 QL_SELL 两个 Q-learning 实例。 策略网络部分,POLICY_NETWORK_BUY 和 POLICY_NETWORK_SELL 都按相同拓扑搭建:首层 AddDenseLayer(9, AF_SIGMOID, 3) 表示 9 个输入神经元、Sigmoid 激活、上一隐层 3 个节点;第二层 AddDenseLayer(3, AF_SOFTMAX) 输出 3 维概率分布。两套网络均用 Init(0.0004, LOSS_BCE) 设定学习率 0.0004 与二分类交叉熵损失。 CNeuralNetwork 的层扩容逻辑靠 ArrayResize + new CLayer 完成:若 LastNeurons 非零则按该值加权重,否则连到前一层激活向量大小。析构时遍历 layers 逐个 delete,避免 MT5 终端内存泄漏。 直接在 MT5 里把 m_scale 从 5 改成 10,输入向量维度会随之翻倍,策略网络首层需同步改 9 为 19 才能编译通过——这是验证拓扑耦合的最快办法。外汇与贵金属杠杆高,此类信号仅作概率参考,实盘须自担风险。

MQL5 / C++
  {  ArrayResize(layers, layers.Size() + class="num">1);
      layers[layers.Size() - class="num">1] = new CLayer(Neurons, AF);
      if(LastNeurons  != class="num">0)
      {  layers[layers.Size() - class="num">1].AddWeights(LastNeurons);
      }
      else if(layers.Size() - class="num">1 > class="num">0)
      {  layers[layers.Size() - class="num">1].AddWeights(layers[layers.Size() - class="num">2].activations.Size());
      }
  };
   class="type">void                 Init(class="type">class="kw">double LearningRate, ENUM_LOSS_FUNCTION LF)
   {  m_loss = LF;
      m_learning_rate = LearningRate;
   };

   vector               Forward(vector& Data);
   class="type">void                 Backward(vector& LabelAnswer);

   class="type">void                 CNeuralNetwork(){};
   class="type">void                 ~CNeuralNetwork()
   {  if(layers.Size() > class="num">0)
      {  for(class="type">int i = class="num">0; i < class="type">int(layers.Size()); i++)
         {  class="kw">delete layers[i];
         }
      }
   };
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSignalTD::CSignalTD(class="type">void) :   m_scale(class="num">5),
   m_use_markov(true),
   m_policy(true)
{
class=class="str">"cmt">//--- initialization of class="kw">protected data
   m_used_series = USE_SERIES_OPEN + USE_SERIES_HIGH + USE_SERIES_LOW + USE_SERIES_CLOSE + USE_SERIES_SPREAD + USE_SERIES_TIME;
   class=class="str">"cmt">//
   RL.actions  = class="num">3;class=class="str">"cmt">//buy, sell, do nothing
   RL.environments = class="num">3;class=class="str">"cmt">//bullish, bearish, flat
   RL.use_markov = m_use_markov;
   RL.epsilon = m_epsilon;
   QL_BUY = new Cql(RL);
   QL_SELL = new Cql(RL);
   class=class="str">"cmt">//
   POLICY_NETWORK_BUY.AddDenseLayer(class="num">9, AF_SIGMOID, class="num">3);
   POLICY_NETWORK_BUY.AddDenseLayer(class="num">3, AF_SOFTMAX);
   POLICY_NETWORK_BUY.Init(class="num">0.0004,LOSS_BCE);
   class=class="str">"cmt">//
   POLICY_NETWORK_SELL.AddDenseLayer(class="num">9, AF_SIGMOID, class="num">3);
   POLICY_NETWORK_SELL.AddDenseLayer(class="num">3, AF_SOFTMAX);
   POLICY_NETWORK_SELL.Init(class="num">0.0004,LOSS_BCE);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalTD::GetOutput(Cql *QL, CNeuralNetwork &PN)
{  class="type">int _td_act = class="num">1;
   vector _in, _in_row, _in_row_old, _in_col, _in_col_old;
   if
   (
      _in_row.Init(m_scale) &&

强化学习环境的差分采样与策略更新

这段逻辑在 MT5 里干的事,是把当前 K 线与上一根做差分,喂给一个 Q-learning 加感知机的混合模型。注意 CopyRates 的偏移参数:行向量取 shift=0,列向量旧集取 shift=m_scale,这意味着列方向拿的是更早一段窗口,用来构造状态空间的横截面特征。 _in_row 与 _in_row_old 都要求 Size()==m_scale+1,否则整段跳过。差分后 Resize(m_scale) 把长度收掉一帧,避免时间维和状态维错位。QL.Environment 吃的是行、列差分向量和全零期望向量 _in_e,随后用末端元素 int(_in_e[m_scale-1]) 定位马尔可夫状态坐标 (_row,_col)。 奖励不是裸价格变动:_reward_float 取末端差分,再除以 _in_row.Max() 与 Min() 构成的区间做归一,QL.GetReward 输出才进策略表。m_policy 开关决定走 OnPolicy 还是 OffPolicy 更新,两套写法都在同一函数内分支,回测时切这个 bool 就能比两种学习范式的信号分布。 标签构造很硬:末端差分大于 0 标 [1,0,0] 看多,小于 0 标 [0,0,1] 看空,等于 0 标 [0,1,0] 横盘。PN.Backward(_label) 反向传播后,再用 policy_history 第 0 行跑前向,取三输出最大下标赋给 _td_act。外汇与贵金属杠杆高,这套信号只代表模型倾向,实盘前请在策略测试器用 2020—2023 年 H1 数据跑一遍确认过拟合程度。

MQL5 / C++
   _in_row.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, m_scale+class="num">1) &&
   _in_row.Size() == m_scale+class="num">1
   &&
   _in_row_old.Init(m_scale) &&
   _in_row_old.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">1, m_scale+class="num">1) &&
   _in_row_old.Size() == m_scale+class="num">1
   &&
   _in_col.Init(m_scale) &&
   _in_col.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, m_scale+class="num">1) &&
   _in_col.Size() == m_scale+class="num">1
   &&
   _in_col_old.Init(m_scale) &&
   _in_col_old.CopyRates(m_symbol.Name(), m_period, class="num">8, m_scale, m_scale+class="num">1) &&
   _in_col_old.Size() == m_scale+class="num">1
  )
  {  _in_row -= _in_row_old;
      _in_col -= _in_col_old;
      _in_row.Resize(m_scale);
      _in_col.Resize(m_scale);
      vector _in_e;
      _in_e.Init(m_scale);
      QL.Environment(_in_row, _in_col, _in_e);
      class="type">int _row = class="num">0, _col = class="num">0;
      QL.SetMarkov(class="type">int(_in_e[m_scale - class="num">1]), _row, _col);
      class="type">class="kw">double _reward_float = _in_row[m_scale - class="num">1];
      class="type">class="kw">double _reward_max = _in_row.Max();
      class="type">class="kw">double _reward_min = _in_row.Min();
      class="type">class="kw">double _reward = QL.GetReward(_reward_max, _reward_min, _reward_float);
      if(m_policy)
      {  QL.SetOnPolicy(_reward, _in_e);
      }
      else if(!m_policy)
      {  QL.SetOffPolicy(_reward, _in_e);
      }
      PN.Forward(QL.policy_history.Row(class="num">1));
      vector _label;
      _label.Init(class="num">3);
      _label.Fill(class="num">0.0);
      if(_in_row[m_scale-class="num">1] > class="num">0.0)
      {  _label[class="num">0] = class="num">1.0;
      }
      else if(_in_row[m_scale-class="num">1] < class="num">0.0)
      {  _label[class="num">2] = class="num">1.0;
      }
      else if(_in_row[m_scale-class="num">1] == class="num">0.0)
      {  _label[class="num">1] = class="num">1.0;
      }
      PN.Backward(_label);
      vector _td_output = PN.Forward(QL.policy_history.Row(class="num">0));
      if(_td_output[class="num">0] >= _td_output[class="num">1] && _td_output[class="num">0] >= _td_output[class="num">2])
      {  _td_act = class="num">0;
      }
      else if(_td_output[class="num">2] >= _td_output[class="num">0] && _td_output[class="num">2] >= _td_output[class="num">1])
      {  _td_act = class="num">2;
      }
  }
   class="kw">return(_td_act);
}

◍ TD 里被忽略的两个敏感参数

前一轮测试只动了 epsilon、马尔可夫权重开关和政策更新开关,长/短条件里的开收盘价阈值、止盈、以点计的开仓阈值也都属于常规非 TD 参数。真正藏在 TD 和强化学习内部的 alpha、gamma 当时被直接按 0.1 与 0.5 写死,没有参与寻优。 这两个值直接决定政策更新的步幅与远期奖励折扣,对信号类整体表现高度敏感,预设值未必落在合适区间。 实现时还略过了政策网络本身的设定:固定用了 3-9-3 结构,各层激活函数和学习率都是常量。实际调参中,单层激活函数或学习率任一变动,甚至全部一起改,都可能显著改变自定义信号类的产出与执行效率。 开 MT5 把 CTrade 信号类里的 alpha、gamma 从 0.1/0.5 起做网格扫描,外汇与贵金属杠杆高、滑点大,回测结论仅代表历史概率,实盘需控仓验证。

「探索衰减之外:让 epsilon 真正活起来」

前面几节把时态差分(TD)强化学习在 MT5 里的落地跑通了,但有一个暗坑没细说:多数示例里 epsilon 随步数单调衰减,隐含假设是模型越往后越不需要探索。可实盘环境里贵金属和外汇的高波动特性,会让这个假设提前失效——价格结构切换时,衰减到谷底的 agent 可能连新形态都采不到样。 更合理的做法不是只减 epsilon,而是让它可变:比如随近期回撤幅度或波动率上调。我们在之前一篇动态学习率文章里验证过,针对 epsilon 做自适应比固定衰减更能保住强化学习的‘根源’。你在 wz_47.mq5 里改几行就能试:把衰减项换成波动率归一后的系数。 外汇与贵金属杠杆高、滑点跳空频繁,任何自适应策略都只是概率占优,不是免死金牌。开 MT5 把附带的 SignalWZ_47.mqh 接上实盘模拟,观察 epsilon 不降反升的时段,往往对应着行情 regime 切换。

常见问题

常漏掉状态归一化与网络输出到Q表索引的映射校验,建议先单品种打印状态分布再接网络。
先固定主帧采样窗口,再把次帧差分作为附加特征而非直接替换回报,能显著降低发散。
可以,小布能基于已加载品种页自动标注Q值突变与采样差分异常,打开对应页即可看到提示。
折扣因子γ与步长λ的轻微偏移会放大时序差,建议网格搜±0.02再定稿。
把衰减绑到近期胜率或波动区间,行情清淡时抬epsilon,趋势明确时压低即可。