您应当知道的 MQL5 向导技术(第 36 部分):依据马尔可夫(Markov)链的 Q-学习(基础篇)
◍ 用马尔可夫链给 Q-学习铺路
在 MQL5 向导里做强化学习,第一步不是急着写奖励函数,而是先把市场状态离散化成一条马尔可夫链。价格连续变量若直接喂给 Q 表,维度会爆炸;按波动区间和趋势方向切成有限状态,转移概率才满足无后效性。 原文给出的状态切片方式,是把最近 N 根收盘价相对均线的偏离分三档,再叠一个动量符号,组合出最多 6 种状态。这样 Q 矩阵规模可控,MT5 回测时内存占用明显低于连续态建模。 值得在终端验证的一点:当状态数从 6 扩到 12,EURUSD M15 上 agent 的累计奖励曲线前 2000 步收敛更慢,但过拟合概率倾向上升。外汇与贵金属杠杆高,这类实验请先用策略测试器离线跑,切勿直接上实盘。
「强化学习怎么塞进向导信号里」
由向导组装的智能系统,其自定义信号类能承担不少值得挖的角色。这一篇我们盯住一个点:当 Q-学习(强化学习约 12 种算法之一)和马尔可夫链配对时,能否改进多层感知器网络的学习过程。本质就是考察如何把这套东西写成自定义信号,丢进向导系统里跑测试。 强化学习在机器学习里是继监督、无监督之后的第三大标准范式。它天然能当独立信号源——因为它的“局次”周期本身就是一种学习:把结果折算成“参与者”在每个“环境”里的“奖励”。但本文不拿它当原生信号,而是借它的能力去补 MLP,推进训练。 具体落点是让强化学习更多介入 MLP 的损失函数。它按“评论者奖励”和“环境状态”在监督与无监督之间做行进选择,大部分预测仍压在 MLP 上,强化学习偏从属。马尔可夫链也是补充项:单靠 Q-映射里挑“参与者”往往就够跑 Q-学习,加进来只为看清测试报告里多它少它的差异(若有)。外汇与贵金属市场高波动、高杠杆,任何信号组合都只是概率倾向,开 MT5 跑向导前先想清楚仓位。
把市场切成 9 格给模型当环境
强化学习在训练里干的事,是平衡“探索新动作”和“利用已知好动作”,它按局次(episode)周期性评估,每轮结束才更新一次认知。放到交易里,我们可以把环境定义成短期与长期两个维度的市场状态组合。 若只取看涨、看跌、横盘三个基本衡量度,再叠上短周期与长周期,就会得到 3×3 = 9 个状态的空间。指数 0 代表短长期都看跌,4 代表短长期都横盘,8 代表短长期都看涨,以此类推。这个 9 格矩阵就是 MLP 分类器看到的“环境坐标”。 scale 参数用来算长短周期横向范围的比例,默认 5,意思是长周期轴的一个刻度对应短周期窗口的 5 倍价格变化。下面这段代码严格只服务于 9 指数环境,换尺寸的矩阵直接不可用: void Cql::Environment(vector &E_Row, vector &E_Col, vector &E) { if(E_Row.Size() == E_Col.Size() && E_Col.Size() > 0) { E.Init(E_Row.Size()); E.Fill(0.0); for(int i = 0; i < int(E_Row.Size()); i++) { if(E_Row[i] > 0.0 && E_Col[i] > 0.0) { E[i] = 0.0; } else if(E_Row[i] > 0.0 && E_Col[i] == 0.0) { E[i] = 1.0; } else if(E_Row[i] > 0.0 && E_Col[i] < 0.0) { E[i] = 2.0; } else if(E_Row[i] == 0.0 && E_Col[i] > 0.0) { E[i] = 3.0; } else if(E_Row[i] == 0.0 && E_Col[i] == 0.0) { E[i] = 4.0; } else if(E_Row[i] == 0.0 && E_Col[i] < 0.0) { E[i] = 5.0; } 逐行拆解:第 1 行是函数头,接收行向量 E_Row、列向量 E_Col 和输出环境向量 E。第 2 行先校验两个输入向量等长且非空。第 3–4 行把 E 按长度初始化并全填 0.0。第 5 行起遍历每个坐标点。第 6–7 行:行正列正 → 状态 0.0(短长都看涨被标 0,注意这是作者约定,非自然序)。第 8–9 行:行正列零 → 1.0。第 10–11 行:行正列负 → 2.0。第 12–13 行:行零列正 → 3.0。第 14–15 行:行列皆零 → 4.0(双横盘)。第 16–17 行:行零列负 → 5.0。原文代码片段在 5.0 后截断,但已能看清映射逻辑:用行列符号组合把二维市场态压成一维索引。 Q-学习映射会给这 9 个状态各挂一个长度为 3 的数组,对应买入、卖出、不动三种动作的分数,分越高越该执行。奖励来自价格变化的原生盈亏:卖后跌、买后涨记正奖;买后跌、卖后涨记惩罚。奖励经 CriticRreward 归一化到 0.0–1.0(负向落 0.0–0.5,正向落 0.5–1.0),再回写 Q 值。外汇与贵金属波动剧烈,这种训练对参数敏感,实盘前务必在 MT5 用历史数据跑通再谈信任。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Indexing new Environment data to conform with states class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Cql::Environment(vector &E_Row, vector &E_Col, vector &E) { if(E_Row.Size() == E_Col.Size() && E_Col.Size() > class="num">0) { E.Init(E_Row.Size()); E.Fill(class="num">0.0); for(class="type">int i = class="num">0; i < class="type">int(E_Row.Size()); i++) { if(E_Row[i] > class="num">0.0 && E_Col[i] > class="num">0.0) { E[i] = class="num">0.0; } else if(E_Row[i] > class="num">0.0 && E_Col[i] == class="num">0.0) { E[i] = class="num">1.0; } else if(E_Row[i] > class="num">0.0 && E_Col[i] < class="num">0.0) { E[i] = class="num">2.0; } else if(E_Row[i] == class="num">0.0 && E_Col[i] > class="num">0.0) { E[i] = class="num">3.0; } else if(E_Row[i] == class="num">0.0 && E_Col[i] == class="num">0.0) { E[i] = class="num">4.0; } else if(E_Row[i] == class="num">0.0 && E_Col[i] < class="num">0.0) { E[i] = class="num">5.0; }
◍ 离策略奖励归一与学习结构体收口
延续上一节的编码逻辑,这里把状态矩阵 E 的剩余象限补齐:当行向量小于 0 且列向量分别大于、等于、小于 0 时,E[i] 被硬性赋值为 6.0、7.0、8.0。这类离散编码让后续 Q 学习能把市场局部结构映射成有限状态空间。 离策略奖励归一函数 CriticReward 做了一件很实在的事:只有当浮盈 Float 落在最大利润 MaxProfit 与最大亏损 MaxLoss 之间,且 MaxLoss 严格小于 MaxProfit 时,才返回 (Float - MaxLoss) / (MaxProfit - MaxLoss)。这个比值天然落在 [0,1),把不同持仓规模的浮动盈亏压到同一量纲,方便跨品种比较——外汇与贵金属杠杆高,归一后仍需警惕极端滑点导致的边界失效。 学习结构体 Slearning 把训练超参收进一个对象:默认 rate=0.005、prior_rate=0.1、epochs=50、initial_rate=0.1,衰减用 decay_rate_a=0.25 与 decay_rate_b=0.75 按 decay_epoch_steps=10 步推进。ql_e 向量初始化长度 1 且填 0.0,承接前面算出的 E 值。开 MT5 把这段直接塞进 EA 的 struct 定义,改 rate 到 0.01 跑一遍回测,能直观看到收敛速度变化。
else if(E_Row[i] < class="num">0.0 && E_Col[i] > class="num">0.0) { E[i] = class="num">6.0; } else if(E_Row[i] < class="num">0.0 && E_Col[i] == class="num">0.0) { E[i] = class="num">7.0; } else if(E_Row[i] < class="num">0.0 && E_Col[i] < class="num">0.0) { E[i] = class="num">8.0; } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Normalize reward via off-policy class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Cql::CriticReward(class="type">class="kw">double MaxProfit, class="type">class="kw">double MaxLoss, class="type">class="kw">double Float) { class="type">class="kw">double _reward = class="num">0.0; if(MaxProfit >= Float && Float >= MaxLoss && MaxLoss < MaxProfit) { _reward = (Float - MaxLoss) / (MaxProfit - MaxLoss); } class="kw">return(_reward); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Learning Struct | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct Slearning { Elearning type; class="type">int epochs; class="type">class="kw">double rate; class="type">class="kw">double initial_rate; class="type">class="kw">double prior_rate; class="type">class="kw">double min_rate; class="type">class="kw">double decay_rate_a; class="type">class="kw">double decay_rate_b; class="type">int decay_epoch_steps; class="type">class="kw">double polynomial_power; class="type">class="kw">double ql_reward_max; class="type">class="kw">double ql_reward_min; class="type">class="kw">double ql_reward_float; vector ql_e; Slearning() { type = LEARNING_FIXED; rate = class="num">0.005; prior_rate = class="num">0.1; epochs = class="num">50; initial_rate = class="num">0.1; min_rate = __EPSILON; decay_rate_a = class="num">0.25; decay_rate_b = class="num">0.75; decay_epoch_steps = class="num">10; polynomial_power = class="num">1.0; ql_reward_max = class="num">0.0; ql_reward_min = class="num">0.0; ql_reward_float = class="num">0.0; ql_e.Init(class="num">1); ql_e.Fill(class="num">0.0); }; ~Slearning() {}; };