您应当知道的 MQL5 向导技术(第 43 部分):依据 SARSA 进行强化学习·进阶篇
📘

您应当知道的 MQL5 向导技术(第 43 部分):依据 SARSA 进行强化学习·进阶篇

第 2/2 篇

◍ 改写 GetOutput 喂给强化学习

把自定义信号类接进 SARSA 智能系统,核心改动落在 GetOutput 上。它不再像旧版那样直接吐信号,而是先拼两组差分特征,再交给 QL 环境对象去算状态与奖励。 代码里用 CopyRates 拉了 4 段长度为 m_scale 的序列:行方向当前/上一根、列方向当前/偏移 m_scale 根。注意第三参数都是 8(即 PRICE_CLOSE),说明特征只吃收盘价;行差分取 0 与 1 根之差,列差分取 0 与 m_scale 根之差。 差分算完塞进 _in_e,由 QL.Environment 生成环境向量;最后一行 _in_e[m_scale-1] 被强制转 int 当作马尔可夫状态索引。奖励则用整段行差分的 max/min 做归一,再交 QL.GetReward 映射。 实测时 epsilon 用优化值即可,只为验证系统能跑通。真上外汇或贵金属实盘前,得按自己对状态—动作权重的理解重调 epsilon,这两类品种杠杆高、跳空频繁,盲用示例参数可能很快被洗出场。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSignalSARSA::GetOutput(class="type">int &Output, Cql *QL)
{  vector _in, _in_row, _in_row_old, _in_col, _in_col_old;
   if
   (
      _in_row.Init(m_scale) &&
      _in_row.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, m_scale) &&
      _in_row.Size() == m_scale
      &&
      _in_row_old.Init(m_scale) &&
      _in_row_old.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">1, m_scale) &&
      _in_row_old.Size() == m_scale
      &&
      _in_col.Init(m_scale) &&
      _in_col.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, m_scale) &&
      _in_col.Size() == m_scale
      &&
      _in_col_old.Init(m_scale) &&
      _in_col_old.CopyRates(m_symbol.Name(), m_period, class="num">8, m_scale, m_scale) &&
      _in_col_old.Size() == m_scale
   )
   {  _in_row -= _in_row_old;
      _in_col -= _in_col_old;
      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);
      QL.SetOnPolicy(_reward, _in_e);
      Output = QL.transition_act;
   }
}

把 SARSA 信号拆开测:Q 映射导出与状态调试

要让自定义 SARSA 信号真正可用,第一步是给它加一个导出 Q-映射矩阵数组的函数。没有这个出口,所谓独立测试和训练就只是空谈;好在策略测试器本身就带交叉验证能力,前向游走测试直接能跑,不必另搭框架。 状态定义值得动刀。把绝对价格水平、RSI、布林带数值这类交替市场状态,再和不同时间帧交叉,能逼出更干净的环境表达。调试时先盯状态变量的捕获与更新逻辑——很多“学不会”的假象,其实只是状态没刷对。 奖励函数得忠实反映交易结果。本文简化版在每根新柱用有利偏移占价格区间的百分比当奖励,边训 Q-映射边按权重下单;这不理想,但能跑。更稳的做法是把奖励拉长到盈利能力和长区间覆盖表现上,而不是柱线级小利。 别盲目追盈利曲线。单测状态-动作对更实在:比如网格坐标 (0,0) 标记看跌短长期条件时,动作 0(卖出)的 Q 权重必须最高,若出现和看跌矛盾的“曲线拟合值”,就是信号写歪了。 ε-贪婪的探索-开发平衡要验,理想 epsilon 得在单独训练集上优化,让 Q-映射备份最佳通验表现;训完再隔离数据集做前向游走,确认或推翻这个 epsilon。策略测试器报告的通验后数据品质,是训练可靠性的现成代理。 若结果优于基准,就在每轮通验结束导出 Q-映射,后续反向训练轮次复用它,相当于给神经网络训练加局次。末轮挑最满意的 Q-映射做单次前向测试,看它在没“看清”的数据上能否复现训练表现——前向游走是 MT5 原生功能。 EURJPY 2022 年日线严苛测试仅为证可用性,结果见下文。外汇与贵金属属高风险,实盘前建议在模拟或纸面实时前瞻里开详细日志,抓市场状态、动作、奖励与 Q 值更新,便于调学习率或 ε-衰减。

「用 SARSA 的 Action 函数拆解波动市应对」

SARSA 属于贴合政策算法,学习时直接把当前动作塞进更新链路,对嘈杂报价流比 Q-学习 更钝感。它的 Q-映射按「与当前动作的差距」成比例更新全部 Q-值,配合 Action 函数里的 ε-贪婪,既探新路也吃老本,市场短期噪声不容易把模型带歪。 代理人挑下一个动作走的是马尔可夫决策:环境状态是一对坐标,经 GetMarkov 压成单一索引,再去马尔可夫矩阵那一行捞概率最高的列,该列索引就是下一状态整数,再拆回行、列坐标。这个矩阵无记忆、不缓冲,高波动时反而比常规指标更跟手。 代码里 act[1]=act[0] 先把上一步动作后移,再写新动作;e_row/e_col 同样做两帧滑动,LetMarkov 把当前环境写进矩阵。遍历 markov[E[0]][i] 取最大者得 _next_state,SetMarkov 拆坐标后,在 Q 里比大小拿 transition_act——0 卖、1 观望、2 买。 输入参数 m_scale 控制单层「时间帧」的缩放,实际只用一根周期线但映射铺了 3 层状态。外汇与贵金属波动剧烈、杠杆风险高,把 epsilon 调大些,SARSA 会偏向随机探更安全动作,极端行情下惨亏的概率可能压住,但别指望它替你预测拐点。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Choose an action class="kw">using epsilon-greedy approach
class=class="str">"cmt">//+------------------------------------------------------------------+
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[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[i][e_row[class="num">0]][e_col[class="num">0]] > _best_value)
         {  _best_value = Q[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">//
   class="type">int _e_row_new = class="num">0, _e_col_new = class="num">0;
   SetMarkov(class="type">int(E[E.Size() - class="num">1]), _e_row_new, _e_col_new);
   e_row[class="num">1] = e_row[class="num">0];
   e_col[class="num">1] = e_col[class="num">0];
   e_row[class="num">0] = _e_row_new;
   e_col[class="num">0] = _e_col_new;
   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);
   transition_act = class="num">0;
   for (class="type">int i = class="num">0; i < THIS.actions; i++)
   {  if(Q[i][_next_row][_next_col] > Q[transition_act][_next_row][_next_col])
      {  transition_act = i;
      }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Getting markov index from environment row & col
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int Cql::GetMarkov(class="type">int Row, class="type">int Col)

◍ 索引计算的一行写法

在矩阵或网格类指标里,常需要把二维坐标压成一维下标。上面这行就是典型实现:用环境数(environments)乘以列号(Col)再加上行号(Row),直接得到线性存储位置。 这种写法把偏移量计算收敛成单行 return,省去临时变量,MT5 编译后几乎零额外开销。你打开自己写的缓冲区索引逻辑,对照看是否也用了「行+环境×列」的基址跳跃方式,不一致可能在多周期切换时错位。 外汇与贵金属品种波动跳变频繁,这类底层索引若算错,画图会静默偏移,回测和实盘信号可能不一致,属于高风险隐蔽 bug。

MQL5 / C++
  class="kw">return(Row + (THIS.environments * Col));

用状态聚合给 SARSA 降维

在外汇和贵金属市场里,状态空间往往又大又连续:价格变动、指标、市场状况、经济日历新闻全搅在一起。SARSA 直接硬算每个细粒度状态不现实,状态空间聚合把相似状态并成“聚合状态”,复杂度立刻降一档。 最粗的玩法就像前文环境那样,只用即时变化和较长跨度变化两个轴、3 个状态。但要做更杂的环境,可以在 Q-映射 的双轴上挂 10 年期收益率、基准利率、PPI、CPI、失业率这些信息——每个轴都能塞多维度宏观变量。 因为 Q-映射 是双轴结构,这套信息对外汇对里的每种货币都得独立适用。与其死磕每个指标的具体读数,不如只判“增 / 平 / 减”三态,像前文给 Q-映射 编索引那样,把结果映射到矩阵每个点。这样代理在 MT5 里跑 SARSA 时,状态数可能从成千上万压到几十,训练收敛概率更高,但宏观误判带来的回撤风险也别忽视。

「记住这一条就够了」

SARSA 与早前研究的 Q-学习、深度-Q-网络同属强化学习在 MT5 上的落地路径,区别只在于权重更新机制是否依赖马尔可夫决策过程。本文随附的 Cql.mqh(10.46 KB)、SignalWZ_43.mqh(7.47 KB)与 wz_43.mq5(6.6 KB)是完整可编译源码,直接丢进 MT5 的 MQL5 目录就能跑通验证。 外汇与贵金属市场高杠杆、高波动,任何 RL 策略都只是概率优势,实盘前务必用历史数据回测并小额试错。 真要把这类算法用顺,核心不是追新模型,而是先搞清你那套权重更新到底吃不吃马尔可夫链——搞反了,回测漂亮实盘也白搭。

常见问题

重写 GetOutput 函数,把指标值归一化后按时间步拼成状态向量,再传入 SARSA 训练循环即可。
单独把 Action 函数抽出来打印每一步选的动作与对应状态,看阈值是否在震荡区间被频繁触发,再调 epsilon 衰减。
可以,小布能按品种页直接加载你的策略脚本,自动导出 Q 映射并标出异常状态聚合维度,省去手动打点。
先用历史分位数把连续状态切 10~20 格,回测中看奖励方差,窄于 5 格通常易过拟合。
一行向量化写法在多数品种上比显式循环快 3~8 倍,仅极长历史数组需注意内存峰值。