您应当知道的 MQL5 向导技术(第 36 部分):依据马尔可夫(Markov)链的 Q-学习·进阶篇
(2/3)· 当强化学习遇上马尔可夫链,MLP 的损失函数还能怎么被推着走?
◍ 在反向传播里切换自定义损失
把神经网络接进 MT5 做行情建模时,损失函数不能只盯均方误差。下面这段枚举给出了三种可插拔选项:LOSS_TYPICAL=-1 走常规梯度,LOSS_SVR=1 用支持向量回归损失,LOSS_QL=2 走 Q-learning 的 critic 回报。 Cmlp::Backward 里根据 THIS.loss_custom 分流:选 SVR 就直接算 _last_loss = SVR_Loss();选 QL 则先由 CriticReward 算原始奖励,再按动作号 0 或 1 对奖励取负或取负绝对值,最后用 Q_Loss() 求梯度。 CriticState 负责离线策略更新 Q 值:取状态向量末位设定新马尔可夫行列下标,滚动保存上一步 e_row/e_col,再 Action() 挑出最大 Q 对应的动作。若开启 use_markov 会调用 LetMarkov 做状态迁移加权。外汇与贵金属波动剧烈,这类模型信号仅具概率意义,实盘前务必在 MT5 策略测试器跑多周期回测。
enum Eloss { LOSS_TYPICAL = -class="num">1, LOSS_SVR = class="num">1, LOSS_QL = class="num">2 }; class="type">void Cmlp::Backward(Slearning &Learning, class="type">int EpochIndex = class="num">1) { ... if(THIS.loss_custom == LOSS_SVR) { _last_loss = SVR_Loss(); } else if(THIS.loss_custom == LOSS_QL) { class="type">class="kw">double _reward = QL.CriticReward(Learning.ql_reward_max, Learning.ql_reward_min, Learning.ql_reward_float); if(QL.act == class="num">0) { _reward *= -class="num">1.0; } else if(QL.act == class="num">1) { _reward = -class="num">1.0 * fabs(_reward); } QL.CriticState(_reward, Learning.ql_e); _last_loss = output.LossGradient(QL.Q_Loss(), THIS.loss_typical); } ... } class="type">void Cql::CriticState(class="type">class="kw">double Reward, vector &E) { 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; class="type">int _new_best_q = Action(); class="type">class="kw">double _weighting = Q[e_row[class="num">0]][e_col[class="num">0]][_new_best_q]; if(THIS.use_markov) { LetMarkov(e_row[class="num">1], e_col[class="num">1], E);
「epsilon-greedy 下的动作选择逻辑」
这段 CQL 类的 Action() 把探索与利用直接压进一行概率判断:当随机数落在 epsilon 区间内就随机出手,否则扫一遍当前状态的 Q 值挑最大的。外汇与贵金属市场的高波动会让 Q 表在样本不足时严重偏移,随机探索比例设太高容易在实盘里频繁吃滑点。 代码先用 rand()%SHORT_MAX 再除以 SHORT_MAX,把整数随机量归一化到 0~1 之间,与 THIS.epsilon 比较。若小于阈值走探索分支,随机选 0 到 actions-1 的任一动作;否则从 Q[e_row[0]][e_col[0]][0] 起步,循环比对后续动作值,记录最大者下标。 最后把选出的 _best_act 写回成员变量 act 并返回。你在 MT5 里接这套逻辑时,建议先把 epsilon 打印到日志观察分布——若连续 100 次调用中探索占比偏离设定值超过 5%,多半是 rand 种子未初始化导致的伪随机聚集。
class="type">int _old_index = GetMarkov(e_row[class="num">1], e_col[class="num">1]); class="type">int _new_index = GetMarkov(e_row[class="num">0], e_col[class="num">0]); _weighting *= markov[_old_index][_new_index]; } Q[e_row[class="num">1]][e_col[class="num">1]][act] += THIS.alpha * (Reward + (THIS.gamma * _weighting) - Q[e_row[class="num">1]][e_col[class="num">1]][act]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Choose an action using epsilon-greedy class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Cql::Action() { 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[e_row[class="num">0]][e_col[class="num">0]][class="num">0]; for (class="type">int i = class="num">1; i < THIS.actions; i++) { if (Q[e_row[class="num">0]][e_col[class="num">0]][i] > _best_value) { _best_value = Q[e_row[class="num">0]][e_col[class="num">0]][i]; _best_act = i; } } } class=class="str">"cmt">//update last action act = _best_act; class="kw">return(_best_act); }
把环境状态压进 9×9 转换矩阵
马尔可夫链本身是无记忆的概率模型:下一状态只取决于当前状态,不翻旧账。在我们的自定义信号里,环境矩阵只装了短期与长期框架各自的看涨、看跌、横盘三种状态,拼成 3×3 共九种组合,因此转换矩阵必须是 9×9,才能把任意一种环境组合映射到另一种。 要把「行+列」这对坐标塞进单一索引,得靠两个互逆函数。GetMarkov 用 Row + (environments * Col) 算出线性位置;SetMarkov 则用 floor(Index / environments) 取列、fmod(Index, environments) 取行还原。environments 固定为 3,所以索引范围 0–8 刚好覆盖九态。 学习时先取旧状态的马尔可夫索引,抽取该列全部转换概率(总和为 1)作为权重;Q-映射选定下一态后,仅用其概率作分子去放大 Q 值。折扣力度与矩阵里的转移概率成正比,概率高的路径学得更快。 每根新柱线触发 LetMarkov 重算:先清掉 markov 与计数矩阵,第一个循环统计 E 序列相邻状态跳转次数,第二个循环用跳转次数除以该状态总出现次数归一化。代码里 _transitions 和 _states 每次重新 Fill(0.0),保证不留历史——这是无记忆设定的硬要求。外汇与贵金属市场高杠杆、跳空频繁,这种纯概率迁移对外汇黄金复盘仅供参考,实盘前务必在 MT5 策略测试器跑一轮。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">// Getting markov index from environment row & col</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">int</span> Cql::GetMarkov(<span class="keyword">class="type">int</span> Row, <span class="keyword">class="type">int</span> Col) { <span class="keyword">class="kw">return</span>(Row + (THIS.environments * Col)); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">// Getting environment row & col from markov index</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> Cql::SetMarkov(<span class="keyword">class="type">int</span> Index, <span class="keyword">class="type">int</span> &Row, <span class="keyword">class="type">int</span> &Col) { Col = <span class="keyword">class="type">int</span>(<span class="functions">floor</span>(Index / THIS.environments)); Row = <span class="keyword">class="type">int</span>(<span class="functions">fmod</span>(Index, THIS.environments)); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">// Function to update markov matrix</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> Cql::LetMarkov(<span class="keyword">class="type">int</span> OldRow, <span class="keyword">class="type">int</span> OldCol, <span class="vector">vector</span> &E) <span class="comment">class=class="str">"cmt">//</span> { <span class="keyword">matrix</span> _transitions; <span class="comment">class=class="str">"cmt">// Count the transitions</span> _transitions.Init(markov.Rows(), markov.Cols()); _transitions.Fill(<span class="number">class="num">0.0</span>); <span class="keyword">vector</span> _states; <span class="comment">class=class="str">"cmt">// Count the occurrences of each state</span> _states.Init(markov.Rows()); _states.Fill(<span class="number">class="num">0.0</span>); <span class="comment">class=class="str">"cmt">// Count transitions from state i to state ii</span> <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < <span class="keyword">class="type">int</span>(E.Size()) - <span class="number">class="num">1</span>; i++) { <span class="keyword">class="type">int</span> _old_state = <span class="keyword">class="type">int</span>(E[i]); <span class="keyword">class="type">int</span> _new_state = <span class="keyword">class="type">int</span>(E[i + <span class="number">class="num">1</span>]); _transitions[_old_state][_new_state]++; _states[_old_state]++; } <span class="comment">class=class="str">"cmt">// Reset prior values to zero.</span> markov.Fill(<span class="number">class="num">0.0</span>); <span class="comment">class=class="str">"cmt">// Compute probabilities by normalizing transition counts</span> <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < <span class="keyword">class="type">int</span>(markov.Rows()); i++) { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> ii = <span class="number">class="num">0</span>; ii < <span class="keyword">class="type">int</span>(markov.Cols()); ii++) { <span class="keyword">if</span> (_states[i] > <span class="number">class="num">0</span>) { markov[i][ii] = <span class="keyword">class="type">class="kw">double</span>(_transitions[i][ii] / _states[i]); } <span class="keyword">else</span> { markov[i][ii] = <span class="number">class="num">0.0</span>; <span class="comment">class=class="str">"cmt">// No transitions from this state</span> } } } }
◍ 把环境坐标喂进 2-8-3 分类器
这套信号类里,强化学习只是配角:它不参与直接出信号,而是把目标函数量化成评论者奖励,主分类器仍是 MLP。环境矩阵是 3×3 网格,横纵轴分别是短期与长期框架从看涨、横盘到看跌的读数,Q 映射复用同一网格并附加动作评分数组,当作 MLP 的训练标签。 输入不是裸价格变化,而是环境状态坐标。短期用单根 K 线变化,长期由整数参数 m_scale 决定倍数——比如 m_scale=8,长期就覆盖最近 8 根柱线的累计变化。GetOutput 里先拉 4 个向量(行/列各新旧两组),相减得到变化,再经 Environment 与 SetMarkov 转成 _row、_col 两个坐标填进输入向量。 MLP 架构固定为 2-8-3:2 个输入、8 节点隐藏层、3 个输出,分别对应做空(索引0)、观望(索引1)、做多(索引2)。输出是一张概率映射,数值越高代表该动作在当前状态适用性可能越强。外汇与贵金属杠杆高,信号仅作概率参考,实盘前请在 MT5 用历史数据跑通后再上模拟盘。 下面这段是 GetOutput 的核心片段,注意 _in_row 与 _in_row_old 都用 CopyRates 取 PERIOD 下的 m_scale 根柱,位置错开一根,相减即短期变化;_in_col 系列同理但取更远端,构成长期变化。最后 _in[0]、_in[1] 直接塞坐标,MLP.Forward() 出概率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalQLM::GetOutput(class="type">int &Output) { m_learning.rate = m_learning_rate; for(class="type">int i = m_epochs; i >= class="num">1; i--) { MLP.LearningType(m_learning, i); for(class="type">int ii = m_train_set; ii >= class="num">0; ii--) { 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, ii + class="num">1, 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, ii + class="num">1 + 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, ii + class="num">1, 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 + ii + class="num">1, 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); MLP.QL.Environment(_in_row, _in_col, _in_e); class="type">int _row = class="num">0, _col = class="num">0; MLP.QL.SetMarkov(class="type">int(_in_e[m_scale - class="num">1]), _row, _col); _in.Init(__MLP_INPUTS); _in[class="num">0] = _row; _in[class="num">1] = _col; MLP.Set(_in); MLP.Forward(); if(ii > class="num">0)