神经网络变得轻松(第二十八部分):政策梯度算法·综合运用
训练循环里的失败即退场逻辑
这段片段处在强化学习训练主循环末尾,负责把临时动作缓冲写入网络并做反向传播,任何一步失败就直接卸载智能体。 TempData.BufferInit 分别以 0 和 1 初始化动作缓冲,再用 Update 把当前批次动作 vActions[batch] 标记为 1 或 0;只要初始化或更新任一返回 false,就调用 ExpertRemove 并 return,EA 当场停止。 backProp(TempData) 若返回 false 同样触发 ExpertRemove,说明梯度回传异常时不会继续浪费 tick。每轮迭代用 PrintFormat 打出 Iteration、Cummulative reward 与 loss,精度到小数点后 5 位,方便在 MT5 日志里观察奖励收敛曲线。 外汇与贵金属市场高波动、高杠杆,这类自学习 EA 在实盘前务必用历史数据多轮回测,训练中断不代表策略失效,但频繁 ExpertRemove 往往指向缓冲或网络维度配置错误。
(!TempData.BufferInit(Actions, class="num">0) || !TempData.Update((class="type">int)vActions[batch], class="num">1)) : (!TempData.BufferInit(Actions, class="num">1) || !TempData.Update((class="type">int)vActions[batch], class="num">0)) )) { ExpertRemove(); class="kw">return; } if(!StudyNet.backProp(TempData)) { ExpertRemove(); class="kw">return; } } PrintFormat("Iteration %d, Cummulative reward %.5f, loss %.5f", iter, cum_reward, loss); } Comment(""); class=class="str">"cmt">//--- ExpertRemove(); }
「把训练好的强化学习模型塞进 MT5 回测」
训练完 DQN 和政策梯度模型后,直接做两个无训练逻辑的测试 EA:Q-learning-test 与 REINFORCE-test。后者在 REINFORCE.mq5 上删掉 Train 函数,把推理搬进 OnTick,只在每根新烛条收盘后跑一次环境评估。 OnTick 里先判新柱,再刷 RSI/CCI/ATR/MACD 历史,填状态缓冲;状态不足(少于 HistoryBars*12 个特征)就跳过。前馈后得到动作概率分布,随机抽一个动作,再查买卖标志决定开平。 DQN 回测出了意外:全程只做了一笔交易,持仓穿整段测试却盈利,图表看它是抓住了全局趋势顺势开仓。但模型用过去 2 年数据训练,而这 2 年品种主跌,它能否及时平仓仍存疑。 政策梯度用贪婪策略结果类似。我改了奖励函数,对无盈利持仓加重罚,重训后超参数试算达到 60% 盈利操作,平均持仓 1 小时 40 分钟。外汇与贵金属杠杆高,回测盈利不预示实盘概率。 下面这段 OnTick 摘录展示了新柱判定与状态拼接的核心,可照抄到自己的推理 EA 里逐行验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { if(lastBar >= iTime(Symb.Name(), TimeFrame, class="num">0)) class="kw">return; class="type">int bars = CopyRates(Symb.Name(), TimeFrame, class="num">0, HistoryBars+class="num">1, Rates); if(!ArraySetAsSeries(Rates, true)) class="kw">return; RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); class=class="str">"cmt">//--- State1.Clear(); for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">int bar_t = (class="type">int)HistoryBars - b; class="type">float open = (class="type">float)Rates[bar_t].open; TimeToStruct(Rates[bar_t].time, sTime); class="type">float rsi = (class="type">float)RSI.Main(bar_t); class="type">float cci = (class="type">float)CCI.Main(bar_t); class="type">float atr = (class="type">float)ATR.Main(bar_t); class="type">float macd = (class="type">float)MACD.Main(bar_t); class="type">float sign = (class="type">float)MACD.Signal(bar_t); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) class="kw">continue; class=class="str">"cmt">//--- if(!State1.Add((class="type">float)Rates[bar_t].close - open) || !State1.Add((class="type">float)Rates[bar_t].high - open) || !State1.Add((class="type">float)Rates[bar_t].low - open) || !State1.Add((class="type">float)Rates[bar_t].tick_volume / class="num">1000.0f) || !State1.Add(sTime.hour) || !State1.Add(sTime.day_of_week) || !State1.Add(sTime.mon) || !State1.Add(rsi) || !State1.Add(cci) || !State1.Add(atr) || !State1.Add(macd) || !State1.Add(sign)) break; } if(State1.Total() < (class="type">int)(HistoryBars * class="num">12)) class="kw">return; if(!StudyNet.feedForward(GetPointer(State1), class="num">12, true)) class="kw">return; StudyNet.getResults(TempData); if(!TempData) class="kw">return; lastBar = Rates[class="num">0].time; class="type">int action = GetAction(TempData); class="kw">delete TempData;
◍ 用循环锁死同品种双向持仓状态
EA 在发单前必须先搞清楚当前图表品种到底有没有活着的多单或空单,否则可能在同一品种上叠出意料外的双向敞口。下面这段逻辑用两个布尔量 Buy、Sell 标记状态,遍历终端全部持仓再按符号过滤。 循环从 i=0 跑到 PositionsTotal() 返回的总持仓数,PositionGetSymbol(i) 取出的品种名若不等于 Symb.Name() 就直接 continue 跳过,只处理本图表品种。随后用 PositionGetInteger(POSITION_TYPE) 强转成 ENUM_POSITION_TYPE,命中 BUY 就把 Buy 置真,命中 SELL 就把 Sell 置真。 action 参数决定动作分支:0 代表想做多,若 Buy 已真就不动,否则先在有 Sell 时平空再开最小手数多单;1 反之;2 是清仓,只要 Buy 或 Sell 任一为真就一键平掉。任一步 Trade 类方法返回失败,就把 lastBar 归零并 return,阻止本根 K 线继续瞎操作。 外汇与贵金属杠杆高,这类持仓状态判断若漏掉 Symbols 过滤,可能在多品种 EA 里误平别人家的单子,上 MT5 用策略测试器跑一遍 EURUSD 便能验证过滤是否生效。
class="type">bool Buy = false; class="type">bool Sell = false; for(class="type">int i = class="num">0; i < PositionsTotal(); i++) { if(PositionGetSymbol(i) != Symb.Name()) class="kw">continue; class="kw">switch((class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: Buy = true; break; case POSITION_TYPE_SELL: Sell = true; break; } } class="kw">switch(action) { case class="num">0: if(!Buy) { if((Sell && !Trade.PositionClose(Symb.Name())) || !Trade.Buy(Symb.LotsMin(), Symb.Name())) { lastBar = class="num">0; class="kw">return; } } break; case class="num">1: if(!Sell) { if((Buy && !Trade.PositionClose(Symb.Name())) || !Trade.Sell(Symb.LotsMin(), Symb.Name())) { lastBar = class="num">0; class="kw">return; } } break; case class="num">2: if(Buy || Sell) if(!Trade.PositionClose(Symb.Name())) { lastBar = class="num">0; class="kw">return; } break; } class=class="str">"cmt">//--- }
策略测试器里跑出来的信号质量
这一篇和前面几篇不同,模型没有只停留在理论推导,而是直接放进 MT5 策略测试器里完成训练和验证。按文中给出的测试结论,由该策略梯度模型生成的信号,如果严格按信号执行,交易操作呈现盈利倾向。 不过有一点必须盯死:奖励函数和损失函数的设定直接决定模型往哪个方向收敛。选错了,策略测试器里跑出的漂亮曲线可能只是过拟合的假象,实盘外汇和贵金属的高波动随时会把漏洞撕开。 文末顺手列了同系列的几篇延伸,第二十五部分讲实践迁移学习,二十六、二十七分别铺了强化学习和深度 Q-学习(DQN)的路子,想接着深挖可以照着编号去翻。
「把工具请下神坛」
这套强化学习示例在 MT5 里落地时,核心就是几个文件各司其职:REINFORCE.mq5 负责带训练逻辑的运行端智能系统,REINFORCE-test.mq5 与 Q-learning-test.mq5 专门丢进策略测试器跑模型验证,NeuroNet.mqh 封装了神经网络类,NeuroNet.cl 则是 OpenCL 侧的实际计算代码,整套压缩包约 73.44 KB。 评论区里有人用 2022 年 9 月 3420 测试版 MT5 编译 VAE.mqh 时,报 'MathRandomNormal' 未声明,作者回了一句「注释掉命名空间 Math 那行」就过了——说明库引用冲突在实盘环境里真会发生,不是文档写错。 外汇和贵金属市场高杠杆、价格序列非平稳,这类模型只是概率意义上的辅助,别当成圣杯。下回你重装终端后若训练报错,先对照文件树和命名空间引用,比盲目调参更省时间。