您应当知道的 MQL5 向导技术(第 43 部分):依据 SARSA 进行强化学习·进阶篇
◍ 改写 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,这两类品种杠杆高、跳空频繁,盲用示例参数可能很快被洗出场。
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 会偏向随机探更安全动作,极端行情下惨亏的概率可能压住,但别指望它替你预测拐点。
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。
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 策略都只是概率优势,实盘前务必用历史数据回测并小额试错。 真要把这类算法用顺,核心不是追新模型,而是先搞清你那套权重更新到底吃不吃马尔可夫链——搞反了,回测漂亮实盘也白搭。