您应当知道的 MQL5 向导技术(第 47 部分):配合时态差异的强化学习·进阶篇
(2/3)· 多数 EA 还在等整局结束才调参,TD 学习凭部分信息增量更新,省下的延迟就是行情里的先手
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 表的强化学习模块在实盘只可能提供概率性信号,复制代码后务必在策略测试器用历史数据验证稳定性。
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 线里状态锁死还是乱跳。
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 周期上短暂优化后,报告显示出该信号类的潜力。外汇与贵金属属高风险品种,任何设置上线实盘前,建议拉长窗口交叉验证,而非直接采信短时回测。
<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 表与网络层能否编译通过。
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)