神经网络变得轻松(第三十四部分):全部参数化的分位数函数·综合运用
(3/3)· 从 IQN 到 FQF,用 MQL5 落地免先验分布的分位数强化学习收尾篇
「FQF 模型在 EURUSD 上的训练与回测表现」
训练用的 FQF-learning.mq5 直接复用前一篇 QRDQN-learning.mq5 的框架,只换了文件名和底层模型对象,架构不再展开。训练数据取 EURUSD 过去 2 年 H1 周期,指标全默认,和本系列其他模型保持一致口径。 训练误差曲线平滑下行,没有剧烈抖动,说明分位数函数参数化这条路在收敛稳定性上没问题。 测试另起一个 FQF-learning-test.mq5,同样是上一篇测试 EA 的改文件名版本。回测结果:盈利因子 1.78,恢复因子 3.7,胜率 57% 以上。最大盈利单接近最大亏损单的 2.5 倍,最长连胜 10 笔、连亏 4 笔,平均盈利比平均亏损高约 ⅓。外汇与贵金属杠杆高,回测盈利不代表实盘概率可复制,上 MT5 跑相同周期前先压小仓位验证。
◍ 训练误差趋稳但别直接上实盘
这套分布式强化学习思路跑下来,参数化分位数函数类的拟合误差在训练期呈稳定收敛趋势,说明网络结构没崩。策略测试器里用训练好的模型回测,能产出正收益曲线,但这是脱离滑点、点差和实时流动性的离线环境。 外汇和贵金属是高风险品种,这种演示型 EA 当前只验证了算法链路可行,离实盘还差实时撮合与风控模块。把它当骨架,自己接行情接口和仓位约束才靠谱。 动手前先在 MT5 策略测试器用不同品种、不同周期重跑训练,确认误差曲线不发散;任何自研工具上真实账户前,都得把样本外数据测透,且默认存在本金回撤概率。
顺着这条线把 RL 读透
想把 MT5 里的强化学习策略真正跑通,光看一两篇入门不够,得按主题链补完底层脉络。从卷积网络打底,到舍弃法防过拟合,再进到 DQN 与政策梯度,这条线在原文系列里对应第 3、12、26、27、28 篇。 分布式 Q-学习把价值估计从期望值扩成完整分布,第 32、33 篇分别讲了基础版和分位数回归版,后者直接把回报分布切成若干分位来训,对尾部风险更敏感。 再往外延伸,分布视角下的隐式分位数网络(IQN)和完全参数化分位数函数,能把分位数做成连续采样,省掉固定分位数的离散约束。外汇与贵金属杠杆高、跳空频繁,用这类分布型 RL 做仓位决策前,建议先在历史 tick 上回测分位覆盖效果,实盘可能仍出现分布外偏离。
「随文发布的工程文件与一段“梦境”代码」
这套 LSTM 优化方案不是只给思路,作者把可直接跑的工程文件一并放出。核心包含 7 个部件:FQF-learning.mq5 与 FQF-learning-test.mq5 是两个 EA,前者做模型优化、后者专供策略测试器回测;FQF.mqh 封装了 FQF 模型类,NeuroNet.mqh 与 NeuroNet.cl 则分别提供神经网络 C++ 类库和 OpenCL 核函数,负责实际建网与加速;NetCreator.mq5 配 NetCreatotPanel.mqh 是可视化的模型构建小工具。 有读者在弱机环境下给每个训练周期末尾加了“睡眠”逻辑,预测准确率提升了约 3%,说明让网络在空数据上前向传播或许能轻微正则化。外汇与贵金属杠杆高,这类实验只代表概率改善,不代表实盘必然增效。 下面这段是原文里附的 Dream 函数片段,用全零缓冲喂给网络模拟“做梦”。逐行看:函数入口 dream 默认 0,注释打印提示;sleep 取 7 或传入值。内层循环按 HistoryBars 数量往 TempData 塞 12 个 0 字段(价、量、指标占位),若总长达不到 HistoryBars*12 就直接返回 0;通过后调 Net.feedForward 和 getResults,再用 TempData.Maximum(0,3) 分支记录信号。想验证就把它插进你自己 EA 的训练收尾,看回测曲线抖动有没有变小。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 梦| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Dream(class="type">int dream = class="num">0) { Comment("!!! Dream !!! "); class="type">int sleep = (dream==class="num">0 ? class="num">7 : dream); for(class="type">int j=class="num">0;j<sleep;j++) { TempData.Clear(); for(class="type">int b=class="num">0; b<(class="type">int)HistoryBars; b++) { if( !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0) || !TempData.Add(class="num">0) || !TempData.Add(class="num">0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) || !TempData.Add(class="num">0.0) ) class="kw">break; } if(TempData.Total()<(class="type">int)HistoryBars*class="num">12) class="kw">return(class="num">0); Net.feedForward(TempData); Net.getResults(TempData); class=class="str">"cmt">//-- 你可以看看 NeuroNet 的 "梦境"。 class="kw">switch(TempData.Maximum(class="num">0,class="num">3)) { case class="num">0: dPrevSignal=TempData[class="num">0]; class="kw">break;
◍ 把这条线请下神坛
上面这段收尾代码把前一轮信号清零后,用三个 0.0 灌入 TempData 再跑一次 backProp,本质只是给网络一个空前向,并非实盘必需动作。Net.Save 在每轮 add_loop 时落盘 .nnw 并打出 Era N -> error X.XX % forecast Y.YY,你可以直接开 MT5 把日志里的 error 曲线拉出来,看它是否在 50 轮内掉到 5% 以下。 ChartScreenShot 固定截 750x400 像素,存成 FileName+count.png,方便你回看每一代「梦境」里的图形状态。Dream(SleepPeriod) 之后打印「梦想周期 = X.XX !」,SleepPeriod 随 Delta++ 递增,意味着越训越慢,这是典型的防过拟合冷却手法。 外汇与贵金属波动受事件驱动,神经网络给出的 forecast 只是概率倾向,实盘仍可能反向击穿。把这条线当参照而非圣旨,每次改 SleepPeriod 步长前先跑十轮空转验证耗时,再决定要不要上真金。
case class="num">1: dPrevSignal=-TempData[class="num">1]; class="kw">break; class="kw">default: dPrevSignal=class="num">0; class="kw">break; } class=class="str">"cmt">//-- ...但这并不是必须的。 class=class="str">"cmt">//--??? TempData.Clear(); TempData.Add(class="num">0.0); TempData.Add(class="num">0.0); TempData.Add(class="num">0.0); Net.backProp(TempData); class=class="str">"cmt">//--??? } class="kw">return(class="num">0); } if(add_loop) count++; if(!stop) { dError=Net.getRecentAverageError(); if(add_loop) { Net.Save(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,true); printf("Era %d -> error %.2f %% forecast %.2f",count,dError,dForecast); } ChartScreenShot(class="num">0,(class="type">class="kw">string)FileName+(class="type">class="kw">string)IntegerToString(count)+".png",class="num">750,class="num">400); } Dream(SleepPeriod); class=class="str">"cmt">//-- Sleep. printf("梦想周期 = %.2f !",SleepPeriod); } SleepPeriod SleepPeriod + (Delta++)