您应当知道的 MQL5 向导技术(第 47 部分):配合时态差异的强化学习·进阶篇
⏱️

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

(2/3)· 多数 EA 还在等整局结束才调参,TD 学习凭部分信息增量更新,省下的延迟就是行情里的先手

新手友好 第 2/3 篇
把蒙特卡洛那套整局跑完再更新的逻辑直接搬进动态行情,是很多自建 EA 的隐性坑。价格动一步环境就变了,等情节结束再修正,策略早已偏离当下盘口。TD 学习从局部信息增量估值,才是贴合 MT5 实时环境的做法。

Q学习里的ε-贪婪动作选择

在 MT5 自定义策略类里,Cql::Action(vector &E) 用 ε-greedy 决定下一步动作:以 epsilon 概率随机探索,否则在 Q_SA 表里挑当前状态价值最高的动作。rand() % SHORT_MAX 再除以 SHORT_MAX,把随机整数压到 0~1 浮点区间,和 THIS.epsilon 比大小。 探索分支直接取 rand() % THIS.actions, exploitation 分支从 i=1 扫到 actions-1,只要 Q_SA[i][e_row][e_col] 大于暂存 _best_value 就更新 _best_act。这样写避免了每次都从 0 号动作起步的比较冗余。 选完动作后,代码把 act[0] 旧值挪到 act[1],新值写 act[0],同步维护 e_row/e_col 的马尔可夫状态转移。LetMarkov 算出下一状态后,再扫 markov 矩阵列找最大转移概率的 _next_state,由 SetMarkov 还原出 _next_row/_next_col。 外汇与贵金属市场波动剧烈、杠杆风险高,这类基于 Q 表的强化学习模块在实盘只可能提供概率性信号,复制代码后务必在策略测试器用历史数据验证稳定性。

MQL5 / C++
class="type">void Cql::Action(vector &E)
{  class="type">int _best_act = class="num">0;
   if (class="type">class="kw">double((rand() % SHORT_MAX) / SHORT_MAX) < THIS.epsilon)
   {  class=class="str">"cmt">// Explore: Choose random action
       _best_act = (rand() % THIS.actions);
   }
   else
   {  class=class="str">"cmt">// Exploit: Choose best action
       class="type">class="kw">double _best_value = Q_SA[class="num">0][e_row[class="num">0]][e_col[class="num">0]];
       for (class="type">int i = class="num">1; i < THIS.actions; i++)
       {  if (Q_SA[i][e_row[class="num">0]][e_col[class="num">0]] > _best_value)
           {  _best_value = Q_SA[i][e_row[class="num">0]][e_col[class="num">0]];
               _best_act = i;
           }
       }
   }
class=class="str">"cmt">//update last action
   act[class="num">1] = act[class="num">0];
   act[class="num">0] = _best_act;
class=class="str">"cmt">//markov decision process
   e_row[class="num">1] = e_row[class="num">0];
   e_col[class="num">1] = e_col[class="num">0];
   LetMarkov(e_row[class="num">1], e_col[class="num">1], E);
   class="type">int _next_state = class="num">0;
   for (class="type">int i = class="num">0; i < class="type">int(markov.Cols()); i++)
   {  if(markov[class="type">int(E[class="num">0])][i] > markov[class="type">int(E[class="num">0])][_next_state])
       {  _next_state = i;
       }
   }
   class="type">int _next_row = class="num">0, _next_col = class="num">0;
   SetMarkov(_next_state, _next_row, _next_col);
   e_row[class="num">0] = _next_row;
   e_col[class="num">0] = _next_col;
   transition_value = Q_V[_next_row][_next_col];
   policy_history[class="num">1][class="num">0] = policy_history[class="num">0][class="num">0];
   policy_history[class="num">1][class="num">1] = policy_history[class="num">0][class="num">1];
   policy_history[class="num">1][class="num">2] = policy_history[class="num">0][class="num">2];
   policy_history[class="num">0][class="num">0] = _next_row;
   policy_history[class="num">0][class="num">1] = transition_value;
   policy_history[class="num">0][class="num">2] = _next_col;
   transition_act = class="num">1;
   for (class="type">int i = class="num">0; i < THIS.actions; i++)
   {  if(Q_SA[i][_next_row][_next_col] > Q_SA[transition_act][_next_row][_next_col])

◍ 马尔可夫状态转移的计数与归一化

更新马尔可夫矩阵的核心,是先统计状态序列里『从 i 到 ii』的实际跳转次数,再做概率归一。下面这段 Cql::LetMarkov 就是干这个的:先建两个临时矩阵/向量,_transitions 记跳转频次,_states 记每个旧状态出现次数。 遍历状态向量 E 时,用 E[i] 当旧状态、E[i+1] 当新状态,对应格子自增 1,同时旧状态计数加 1。注意循环只跑到 E.Size()-1,因为最后一项没有『下一个状态』可跳。 清零旧 markov 后,用 _transitions[i][ii] 除以 _states[i] 得到条件概率;若某状态一次没出现(_states[i]==0),该行直接填 0.0,避免除零。外汇与贵金属行情里状态切换受消息面扰动大,这套概率矩阵仅反映历史样本,下一段行情延续该转移倾向的概率会随波动率变化。 开 MT5 把这段代码塞进你的 EA,打印 markov 矩阵看几行,就能知道最近 N 根 K 线里状态锁死还是乱跳。

MQL5 / C++
class="type">void Cql::LetMarkov(class="type">int OldRow, class="type">int OldCol, vector &E)  class=class="str">"cmt">//
{  matrix _transitions;  class=class="str">"cmt">// Count the transitions
   _transitions.Init(markov.Rows(), markov.Cols());
   _transitions.Fill(class="num">0.0);
   vector _states;  class=class="str">"cmt">// Count the occurrences of each state
   _states.Init(markov.Rows());
   _states.Fill(class="num">0.0);
class=class="str">"cmt">// Count transitions from state i to state ii
   for (class="type">int i = class="num">0; i < class="type">int(E.Size()) - class="num">1; i++)
   {  class="type">int _old_state = class="type">int(E[i]);
      class="type">int _new_state = class="type">int(E[i + class="num">1]);
      _transitions[_old_state][_new_state]++;
      _states[_old_state]++;
   }
class=class="str">"cmt">// Reset prior values to zero.
   markov.Fill(class="num">0.0);
class=class="str">"cmt">// Compute probabilities by normalizing transition counts
   for (class="type">int i = class="num">0; i < class="type">int(markov.Rows()); i++)
   {  for (class="type">int ii = class="num">0; ii < class="type">int(markov.Cols()); ii++)
      {  if (_states[i] > class="num">0)
         {  markov[i][ii] = class="type">class="kw">double(_transitions[i][ii] / _states[i]);
         }
         else
         {  markov[i][ii] = class="num">0.0;  class=class="str">"cmt">// No transitions from this state
         }
      }
   }
}

「TD 在非金融场景里的增量优势」

TD 的 Q-值更新频率高于蒙特卡洛,在快速变化的流动环境里,它只更新状态值、不绑定“状态-动作”对,这和 SARSA、Q-学习从根上不同。看几个日常系统就能明白它为什么省算力。 仓库库存管理若只盯总体库存水平、不评每个 SKU 的订购动作,TD 直接更新状态数值函数,无需政策网络 MLP,也不用算每笔可能的订购决策。库存渐变而非突变反馈时,TD 的增量更新正好吃住平滑过渡;换成大“状态-动作”空间的复杂库存,SARSA/Q-学习必然抬升计算成本,TD 因结构更简单从不会卡这一步。 智能建筑 HVAC 调温要平衡短期电费与长期舒适度,能耗和舒适度是绝对指标、不挂具体动作。TD 可跑两个并行周期分别预测,用增量每周期更新(非蒙特卡洛的每局次)无缝跟住温度、占用率、空气品质的渐变;等效 SARSA 方案得额外吃计算和内存去“补救”状态值偏差。 交通流量预测只关心拥堵水平这种整体状态,TD 学总体值而非单信号影响;流量全天动态,TD 不必等一局跑完再更新。小城市路网交织时,计算和内存过载会成实打实瓶颈,TD 在这方面能直接削掉一块负担。 制造厂预测性维护要拿部分反馈续写机器健康值,机器老化是渐变过程,TD 靠增量传感器数据持续更新,不似 SARSA 非跟踪“维修或更换”动作;多机工厂扩缩容时,它只跟每台状态、不存全量“状态-动作”对,扩展性自然好。这些是金融交易之外的用例,下步看怎么落进自定义信号类。

TD 信号类里的双网络与增量学习

TD 算法的自定义信号类不再只靠强化学习环境做决策。它在买方、卖方各挂一个 CQL 实例处理强化学习,同时各配一个政策网络(MLP),由神经网络预测卖、持、买三类动作的概率分布。 初始化时,政策网络作为 GetOutput 的输入之一,使系统能做在线增量学习:不按批次训练,只用最新一根柱线数据跑一次反向传播和一次前向投喂。但因反向传播前需把当前柱价载入网络,会先用前一根柱线信息做一次前向运行来加载状态。 Forward 函数返回长度为 3 的输出向量,索引 0/1/2 分别对应卖出、持有、买入的阈值或概率。环境矩阵 Q_V 在此只给特定环境赋值,不含逐动作 Q 值;下一步动作由政策网络依据「环境数值(该状态下所有动作 Q 值之和)+ 状态坐标」预测得出。 配合 MQL5 向导编译成 EA,在 GBPUSD 的 2022 年 H1 周期上短暂优化后,报告显示出该信号类的潜力。外汇与贵金属属高风险品种,任何设置上线实盘前,建议拉长窗口交叉验证,而非直接采信短时回测。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Q_SA-Learning Class Interface.                                                  |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> Cql
{
<span class="keyword">class="kw">protected</span>:
   <span class="keyword">matrix</span>                markov;          class=class="str">"cmt">// 马尔可夫转移矩阵,记录状态跳转
   <span class="keyword">class="type">void</span>                LetMarkov(class="type">int OldRow, class="type">int OldCol, vector &E); class=class="str">"cmt">// 写入旧状态行列对应的转移
   <span class="keyword">vector</span>               acts;            class=class="str">"cmt">// 可用动作向量
   <span class="keyword">matrix</span>               environment;     class=class="str">"cmt">// 环境映射矩阵
   <span class="keyword">matrix</span>               Q_SA[];         class=class="str">"cmt">// 状态-动作 Q 值矩阵数组
   <span class="keyword">matrix</span>               Q_V;            class=class="str">"cmt">// TD 用的环境值矩阵(无逐动作 Q)
<span class="keyword">class="kw">public</span>:
   <span class="keyword">class="type">void</span>                Action(vector &E);                class=class="str">"cmt">// 执行动作
   <span class="keyword">class="type">void</span>                Environment(vector &E_Row, vector &E_Col, vector &E); class=class="str">"cmt">// 设置环境行列
   <span class="keyword">class="type">void</span>                SetOffPolicy(class="type">class="kw">double Reward, vector &E); class=class="str">"cmt">// 离策略更新
   <span class="keyword">class="type">void</span>                SetOnPolicy(class="type">class="kw">double Reward, vector &E);  class=class="str">"cmt">// 在策略更新
   <span class="keyword">class="type">class="kw">double</span>              GetReward(class="type">class="kw">double MaxProfit, class="type">class="kw">double MaxLoss, class="type">class="kw">double Float); class=class="str">"cmt">// 计算奖励
   <span class="keyword">vector</span>               SetTarget(vector &Rewards, vector &TargetOutput); class=class="str">"cmt">// 设训练目标
   <span class="keyword">class="type">void</span>                SetMarkov(class="type">int Index, class="type">int &Row, class="type">int &Col); class=class="str">"cmt">// 设转移索引
   <span class="keyword">class="type">int</span>                 GetMarkov(class="type">int Row, class="type">int Col);          class=class="str">"cmt">// 读转移矩阵值
   Sql                 THIS;            class=class="str">"cmt">// 自身引用
   <span class="keyword">class="type">int</span>                 act[class="num">2];          class=class="str">"cmt">// 买卖方动作缓存
   <span class="keyword">class="type">int</span>                 e_row[class="num">2];        class=class="str">"cmt">// 环境行坐标
   <span class="keyword">class="type">int</span>                 e_col[class="num">2];        class=class="str">"cmt">// 环境列坐标
   <span class="keyword">class="type">int</span>                 transition_act;  class=class="str">"cmt">// 转移动作
   <span class="keyword">class="type">class="kw">double</span>              transition_value;class=class="str">"cmt">// 转移值
   <span class="keyword">matrix</span>               policy_history;  class=class="str">"cmt">// 政策历史记录
};

◍ Q表与神经网络类的初始化衔接

这段结构把强化学习的 Q 表容器和后续神经网络类拼到了一起。Cql 构造里先判断 RL.actions 与 RL.environments 都大于 0 才往下走,否则整张表不初始化,实盘里若传入空环境参数会直接跳过建表。 Q_SA 用 ArrayResize 按动作数展开,每个动作 i 对应一张 environments×environments 的矩阵,acts[i] 赋值为 i+1 作为动作索引。environment 矩阵则按 i*environments+ii+1 填值,把二维状态铺平成一维编号,方便后面 markov 转移矩阵对齐。 policy_history 初始化为 2 行 3 列并填 0,e_row、e_col、act 三个数组分别用 ArrayFill 清成 0、0、1,transition_act 固定为 1,这意味着默认从动作 1 起步转移。外汇与贵金属行情下用这类表做状态映射时,环境数若设得过大,矩阵规模会按平方膨胀,回测内存占用需提前估。 CNeuralNetwork 类只露了 TransposeToCol、TransposeToRow 两个保护方法,以及 AddDenseLayer 公开接口,默认激活函数为 AF_RELU、LastNeurons 为 0,说明层间维度靠调用时推断。把这段代码直接贴进 MT5 头文件,能验证 Q 表与网络层能否编译通过。

MQL5 / C++
vector Q_Loss()
 { vector _loss;
   _loss.Init(THIS.actions);
   _loss.Fill(class="num">0.0);
   for(class="type">int i = class="num">0; i < THIS.actions; i++)
   { _loss[i] = Q_SA[e_row[class="num">0]][e_col[class="num">0]][i];
   }
   class="kw">return(_loss);
 }
 class="type">void Cql(Sql &RL)
 { class=class="str">"cmt">//
   if(RL.actions > class="num">0 && RL.environments > class="num">0)
   { policy_history.Init(class="num">2,class="num">2+class="num">1);
     policy_history.Fill(class="num">0.0);
     acts.Init(RL.actions);
     ArrayResize(Q_SA, RL.actions);
     for(class="type">int i = class="num">0; i < RL.actions; i++)
      { acts[i] = i + class="num">1;
        Q_SA[i].Init(RL.environments, RL.environments);
      }
     Q_V.Init(RL.environments, RL.environments);
     environment.Init(RL.environments, RL.environments);
     for(class="type">int i = class="num">0; i < RL.environments; i++)
      { for(class="type">int ii = class="num">0; ii < RL.environments; ii++)
        { environment[i][ii] = ii + (i * RL.environments) + class="num">1;
        }
      }
     markov.Init(RL.environments * RL.environments, RL.environments * RL.environments);
     markov.Fill(class="num">0.0);
     THIS = RL;
     ArrayFill(e_row, class="num">0, class="num">2, class="num">0);
     ArrayFill(e_col, class="num">0, class="num">2, class="num">0);
     ArrayFill(act, class="num">0, class="num">2, class="num">1);
     transition_act = class="num">1;
   }
 };
 class="type">void ~Cql(class="type">void) {};
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CNeuralNetwork
{
class="kw">protected:
  matrix TransposeToCol(vector &V);
  matrix TransposeToRow(vector &V);
class="kw">public:
  CLayer *layers[];
  class="type">class="kw">double m_learning_rate;
  ENUM_LOSS_FUNCTION m_loss;
  class="type">void AddDenseLayer(class="type">class="kw">ulong Neurons, ENUM_ACTIVATION_FUNCTION AF = AF_RELU, class="type">class="kw">ulong LastNeurons = class="num">0)
把重复估值交给小布盯盘
这些 TD 状态值的增量诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到局部更新的估值漂移,你只管判断政策要不要切。

常见问题

蒙特卡洛要等整局奖励收集完才单次更新,TD 基于下一个状态估值和即时奖励增量更新 V(s),无需等待情节结束,更适合动态盘。
可以,小布盯盘内置了这类局部更新估值的 AIGC 视图,对应品种页会展示 TD 类算法的估值变化,省去自己写矩阵跟踪的代码。
TD 更新的是状态值 V(s),不绑定具体动作对,所以既能套无政策公式追最优,也能依当前行为政策增量走,比 SARSA 灵活。
Q_V 跟踪与动作无关的状态估值,对应 TD 关注点;Q_SA 保留状态-动作对数值,每次函数调用都更新,二者映射同一环境坐标。
γ 偏高会放大未来奖励权重,局部更新容易被远端估值带偏,在外汇贵金属这种高波动品种上可能让政策收敛变慢。