您应当知道的 MQL5 向导技术(第 41 部分):深度-Q-网络·进阶篇
(2/3)· 把深度-Q-网络塞进向导信号类,别让训练抖动毁了你的做多做空判定
接上篇 Q-学习的基础,我们继续深挖向导里的深度-Q-网络实现。不少交易者直接拿原始 DQN 套信号类,训练时 q-值来回跳,最终智能系统在回测里过拟合却实盘失灵。这一篇先把网络结构和训练稳定性讲透,再动手改代码。
「DQN 信号类的析构与受保护接口」
在 MT5 里写一个基于深度 Q 网络(DQN)的信号类,第一步是把资源回收和内部推理接口先钉死。上面这段头文件碎片里,~CSignalDQN(void) 是显式析构声明,用来在 EA 卸载或重启时释放三个神经网络指针:MLP、DQN_ONLINE、DQN_TARGET。 protected 段里的 GetOutput(int &Output) 不对外暴露,只供类内部把网络前向计算的结果写回引用形参。这样做能把“在线网络”和“目标网络”的权重更新逻辑关在类里,避免外部随意篡改导致训练发散。 实盘接这类信号前,建议先在策略测试器用 2020—2023 年 XAUUSD 的 M15 数据跑一遍空壳类,确认析构不报“对象已删除”错误再填网络代码。外汇与贵金属杠杆高,信号类任何内存泄漏都可能让终端在长周期挂机中崩溃。
class="type">void ~CSignalDQN(class="type">void); class=class="str">"cmt">//--- methods of setting adjustable parameters ... ... class="kw">protected: class="type">void GetOutput(class="type">int &Output); Cmlp *MLP,*DQN_ONLINE,*DQN_TARGET; };
◍ 目标网络怎么压住DQN的振荡
目标网络和在线网络结构一致:吃环境状态,吐每个动作的 q-值。区别在于它算的是「下一状态」的 q-值,而且不反向传播,只在固定间隔直接拷贝在线网络的权重。这样算时序差分时目标值更稳,训练发散的概率明显下降。 如果不隔离目标网络,在线网络和母级 MLP 会同步更新,目标值跟着乱跳,振荡会自我放大。原文里用于调制的参数标记成 'm_target_counts',默认只给 65——因为他们只在日线跑了一年,总共才 260 根柱线。若换更长周期或更小周期,10,000 步左右的间隔才合理,这是个可改的旋钮。 引导问题是另一层坑:传统 Q-学习用自己预测的 q-值去更新自己,误差会沿状态链滚雪球。目标网络靠低频更新(慢权重)切断这条链,让误差传播慢下来。金融市场这种强非线性环境里,没它 DQN 更容易直接发散。 双 DQN 是顺手能加的扩展:在线网络选动作,目标网络评动作,把「选」和「评」拆开,缓解高估偏差。下面这段 MQL5 是目标 q-值的直接计算,挂在我们之前 CQL 类里。 代码逐行拆: //+------------------------------------------------------------------+ // Critic Target for DQN —— 注释:DQN 的评论家目标函数 //+------------------------------------------------------------------+ vector Cql::CriticTarget(vector &Rewards, vector &TargetOutput) —— 定义类方法,输入即时奖励向量 Rewards 和下一状态目标网络输出 TargetOutput,返回向量 { vector _target = Rewards + (THIS.gamma * TargetOutput); —— 按公式 y = r + γ·max Q_target 计算,THIS.gamma 是折扣系数 return(_target); —— 把算好的目标 q-值向量返回给调用方 }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Critic Target for DQN class=class="str">"cmt">//+------------------------------------------------------------------+ vector Cql::CriticTarget(vector &Rewards, vector &TargetOutput) { vector _target = Rewards + (THIS.gamma * TargetOutput); class="kw">return(_target); }
回放缓冲如何压住金融序列的时态耦合
DQN 把每一步的(状态, 动作, 奖励, 下一状态)四元组写进回放缓冲,训练时再随机小批量抽取来更新权重与乖离。金融市场的价格、成交量、波动率天然高度自相关,若直接按时间顺序喂给网络,代理人容易记住某段特定行情的形态,换环境就失效——这是过度拟合的典型来源。 随机采样把相邻样本打散,经验分布更接近市场底层动态,权重更新的方差明显下降。实测中,去掉回放、只用连续样本训练时,单步梯度会突然爆发,又被极少数异质样本打断,损失曲线剧烈抖动;接入回放后更新轨迹平滑得多,收敛步数倾向更短。 缓冲还有复用价值:同一条历史经验可能被抽中多次,罕见事件如崩盘、急拉虽不在当下,也能被反复学习。优先经验回放(PER)进一步按 TD 误差排权重,误差大的样本优先入批,学习效率更高。 这套机制是 DQN 的骨架,但 MQL5 里怎么落地、怎么把 DQN 做成信号主模型,留到后续篇章用代码拆。
「用 Gamma 平衡即时与滞后回报」
做决策的影响常有延迟,不只交易,机器人或游戏里也一样,动作的效果会在更长横截面里慢慢释放。交易者看经济新闻和公司公示,本质是在处理一种时间依赖:现在开的仓,代价或收益可能隔很久才结账。 DQN 的 q-值方程里用 Gamma 因子刻画这种时态关系。Gamma 在短期奖励和长期奖励之间取平衡,让 q-值能往前多看几步,估测一个动作随时间的累积影响。部分奖励被推迟时,Gamma 保证代理人不无视长期回报,同时仍盯紧眼前信号——外汇和贵金属市场高波动、高杠杆,这种兼顾能力可能是本质性的。 典型现象:重大利率决策公布时开仓,开头往往是逆着预期的不利走位区间,不会立刻出现有利位移。若信号具备向前看的能力(如 DQN 架构),才可能在滞后回报兑现时占到优势。
◍ 把 DQN 塞进损失函数里
要让混合损失函数跑起 DQN,第一步是给自定义损失枚举补一个选项。下面代码里 LOSS_DQN = 3 就是新增的标识,选它时反向传播会走 DQN 的增量计算分支,而不是原先的 SVR 或基础 QL 路径。 反向传播函数 Backward 里,关键改动在 delta 计算段:当 THIS.loss_custom == LOSS_DQN 时,先调 QL.CriticReward 算出标量奖励 _reward,再初始化与 dqn_target 同尺寸的 _rewards 向量并填 0,若奖励为正就把下标 0 置 1。这说明 DQN 分支把奖励信号压缩成稀疏向量去驱动梯度,而非连续回归。 CQL 类里的 q-值公式已高亮,GetOutput 生成条件阈值的方式和早前基础 q-学习那篇差别不大。架构上摆了在线与目标两个 DQN 网络,父级 MLP 仍吃收盘价变化做输入;预测不靠 q-映射,而是用目标网络训出来的在线 DQN 去估动作 q-值向量。 开 MT5 把枚举和 Backward 的 LOSS_DQN 分支抄进你的 mlp 类,先拿 EURUSD 的 H1 收盘差分跑 200 轮,观察 _rewards[0] 触发频率——若长期为 0,说明奖励区间设得偏紧,QL.CriticReward 的 max/min 参数得往窄调。外汇与贵金属杠杆高,这类实验仅作算法验证,实盘概率性失效属正常。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom Loss-Function Enumerator | class=class="str">"cmt">//+------------------------------------------------------------------+ enum Eloss { LOSS_TYPICAL = -class="num">1, LOSS_SVR = class="num">1, LOSS_QL = class="num">2, LOSS_DQN = class="num">3 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| BACKWARD PROPAGATION OF THE MULTI-LAYER-PERCEPTRON. | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| -Extra Validation check of MLP architecture settings is performed| class=class="str">"cmt">//| at run-time. | class=class="str">"cmt">//| Chceking of &class="macro">#x27;validation&class="macro">#x27; parameter should ideally be performed | class=class="str">"cmt">//| at class instance initialisation. | class=class="str">"cmt">//| | class=class="str">"cmt">//| -Run-time Validation of learning rate, decay rates and epoch | class=class="str">"cmt">//| index is performed as these are optimisable inputs. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Cmlp::Backward(Slearning &Learning, class="type">int EpochIndex = class="num">1) { if(!validated) { printf(__FUNCSIG__ + " invalid network arch! "); class="kw">return; } .... class=class="str">"cmt">//COMPUTE DELTAS vector _last, _last_derivative; _last.Init(inputs.Size()); if(hidden_layers == class="num">0) { _last = weights[hidden_layers].MatMul(inputs); } else if(hidden_layers > class="num">0) { _last = weights[hidden_layers].MatMul(hidden_outputs[hidden_layers - class="num">1]); } _last.Derivative(_last_derivative, THIS.activation); vector _last_loss = output.LossGradient(label, THIS.loss_typical); if(THIS.loss_custom == LOSS_SVR) { _last_loss = SVR_Loss(); } else if(THIS.loss_custom == LOSS_QL) { .... } else if(THIS.loss_custom == LOSS_DQN) { class="type">class="kw">double _reward = QL.CriticReward(Learning.ql_reward_max, Learning.ql_reward_min, Learning.ql_reward_float); vector _rewards; _rewards.Init(Learning.dqn_target.Size()); _rewards.Fill(class="num">0.0); if(_reward > class="num">0.0) { _rewards[class="num">0] = class="num">1.0;