神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式·综合运用
◍ 训练循环里的状态快照与收盘落盘
这段逻辑跑在 EA 的初始化收尾与逐笔 OnTick 里,核心是把每根 K 线的多因子差值塞进神经网络状态容器,并在退出时把权重落盘。 训练进度通过 Comment 实时刷在图表左上角:StringFormat 把当前迭代百分比(iter*100.0/Iterations)和 StudyNet.getRecentAverageError() 的 8 位误差一起打印,方便肉眼看收敛。GetTickCount 卡在循环里做耗时采样,跑完直接 ExpertRemove 结束 EA 生命周期。 OnDeinit 里先释放 Rewards 指针,再调 StudyNet.Save(FileName+".nnw",0,0,0,0,true) 把网络以 .nnw 后缀存盘——这意味着你下次加载可以跳过训练直接推理。外汇与贵金属品种下这类自学习 EA 实盘误差可能漂移,属高风险用法。 OnTick 只在 IsNewBar 通过后处理。它用 AccountInfoDouble(ACCOUNT_EQUITY) 取浮动权益,当 State1.Total() 达到 HistoryBars*12+9 这个固定维度时,把 (current-Equity) 作为奖励差交给 cReplay.AddState。随后 CopyRates 拉 HistoryBars 根数据,ArraySetAsSeries 置为时间倒序。 指标刷新后进入 b 循环:每根 bar 提取 open/close-high-low 差值、tick_volume/1000、以及小时/星期/月份结构体分量,再拼上 RSI、CCI、ATR、MACD 主线与信号线共 5 个浮点。任一指标为 EMPTY_VALUE 就 continue 跳过该 bar,否则通过 State1.Add 逐项入栈。
Comment(StringFormat("%.2f%% -> Error %.8f", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), StudyNet.getRecentAverageError())); ticks = GetTickCount(); } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %class="num">10.7f", __FUNCTION__, __LINE__, StudyNet.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { if(!!Rewards) class="kw">delete Rewards; class=class="str">"cmt">//--- StudyNet.Save(FileName + ".nnw", class="num">0, class="num">0, class="num">0, class="num">0, true); } class="type">void OnTick() { if(!IsNewBar()) class="kw">return; class=class="str">"cmt">//--- class="type">class="kw">float current = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_EQUITY); if(Equity >= class="num">0 && State1.Total() == (HistoryBars * class="num">12 + class="num">9)) cReplay.AddState(GetPointer(State1), Action, (class="type">class="kw">double)(current - Equity)); Equity = current; class=class="str">"cmt">//--- class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">return; } class=class="str">"cmt">//--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); State1.Clear(); for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">class="kw">float open = (class="type">class="kw">float)Rates[b].open; TimeToStruct(Rates[b].time, sTime); class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(b); class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(b); class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(b); class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(b); class="type">class="kw">float sign = (class="type">class="kw">float)MACD.Signal(b); 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">class="kw">float)Rates[b].close - open) || !State1.Add((class="type">class="kw">float)Rates[b].high - open) || !State1.Add((class="type">class="kw">float)Rates[b].low - open) || !State1.Add((class="type">class="kw">float)Rates[b].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)) {
「把账户与持仓喂给神经网络再下单」
这段逻辑把账户状态和当前品种持仓压缩成一组 float 特征,塞进名为 State1 的向量里。注意 Add 链只要有一个失败就直接 return,说明特征缺失时绝不让模型盲猜——外汇与贵金属杠杆高,残缺状态驱动决策可能放大回撤。 持仓遍历只认 Symb.Name() 的品种:多单累加 volume 与 profit 到 buy_value / buy_profit,空单同理进 sell_value / sell_profit。但原文空单分支误写了 return 而非 break,会导致遍历遇空单提前退出,sell_profit 可能漏算,开 MT5 跑前务必改成 break。 特征齐了才调 StudyNet.feedForward(..., 12, true),12 是隐藏层节点数,getAction() 返回 0/1/2 分别触发 Buy、Sell、平掉本品种所有仓。值得盯的是 case 2 用 PositionGetInteger(POSITION_IDENTIFIER) 关仓,倒序遍历 PositionsTotal() 避免索引错位。 末尾取 TimeCurrent 的 hour,等于 0 时进入一段未完的分支,通常用于跨日重置或日志切分。把这段直接贴进 EA 的 OnTick 尾段,能验证特征向量维度与网络输出是否对齐。
PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } } class=class="str">"cmt">//--- if(!State1.Add((class="type">class="kw">float)AccountInfoDouble(ACCOUNT_BALANCE)) || !State1.Add((class="type">class="kw">float)AccountInfoDouble(ACCOUNT_EQUITY)) || !State1.Add((class="type">class="kw">float)AccountInfoDouble(ACCOUNT_MARGIN_FREE)) || !State1.Add((class="type">class="kw">float)AccountInfoDouble(ACCOUNT_MARGIN_LEVEL)) || !State1.Add((class="type">class="kw">float)AccountInfoDouble(ACCOUNT_PROFIT))) class="kw">return; class=class="str">"cmt">//--- class="type">class="kw">double buy_value = class="num">0, sell_value = class="num">0, buy_profit = class="num">0, sell_profit = class="num">0; class="type">int total = PositionsTotal(); for(class="type">int i = class="num">0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) class="kw">continue; class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += PositionGetDouble(POSITION_PROFIT); class="kw">break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += PositionGetDouble(POSITION_PROFIT); class="kw">return; } } if(!State1.Add((class="type">class="kw">float)buy_value) || !State1.Add((class="type">class="kw">float)sell_value) || !State1.Add((class="type">class="kw">float)buy_profit) || !State1.Add((class="type">class="kw">float)sell_profit)) class="kw">return; if(!StudyNet.feedForward(GetPointer(State1), class="num">12, true)) class="kw">return; Action = StudyNet.getAction(); class="kw">switch(Action) { case class="num">0: Trade.Buy(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">1: Trade.Sell(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">2: for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) if(PositionGetSymbol(i) == Symb.Name()) Trade.PositionClose(PositionGetInteger(POSITION_IDENTIFIER)); class="kw">break; } class="type">MqlDateTime time; TimeCurrent(time); if(time.hour == class="num">0) {
回放缓冲里的十步训练循环
这段代码把经验回放(replay buffer)里的样本喂给神经网络做强化学习更新,每轮固定抽 10 条历史状态转移来训练,避免在线学习对近期样本过拟合。 循环里先通过 cReplay.GetRendomState 取出一条随机转移:当前状态 pstate1、执行动作 repl_action、奖励 repl_reward 和下一状态 pstate2;任意一步取数失败就直接 return 跳出,保证训练数据完整。 前向传播用 StudyNet.feedForward(pstate1, 12, true) 跑 12 个输出的网络并保留中间量;getResults 把输出写进 Rewards,再用 Rewards.Update 把实际奖励写回对应动作位置。最后 backProp 带着DiscountFactor 和 pstate2 做反向传播,同样 12 输出、true 标记训练模式。外汇与贵金属市场噪声大、跳空频繁,这种回放机制能平滑单笔极端行情对策略梯度的冲击,但过拟合历史样本的风险仍偏高,实盘前务必在 MT5 用不同年份数据重跑验证。
class="type">int repl_action; class="type">class="kw">double repl_reward; for(class="type">int i = class="num">0; i < class="num">10; i++) { if(cReplay.GetRendomState(pstate1, repl_action, repl_reward, pstate2)) class="kw">return; if(!StudyNet.feedForward(pstate1, class="num">12, true)) class="kw">return; StudyNet.getResults(Rewards); if(!Rewards.Update(repl_action, (class="type">class="kw">float)repl_reward)) class="kw">return; if(!StudyNet.backProp(GetPointer(Rewards), DiscountFactor, pstate2, class="num">12, true)) class="kw">return; } } class=class="str">"cmt">//--- }
◍ EURUSD H1 上的三阶段回测表现
三个 EA 按 Go-Explore 思路接力跑通:先用策略测试器优化模式反复启第一阶段 EA 建样本存档,再让第二阶段 EA 做若干轮策略迭代,最后用强化学习在测试器里收尾优调。整条链路只用 EURUSD 的 H1 历史数据,指标全走默认参数,没动过任何输入。 回测图形看着是一路平缓向上的余额曲线,没有大回撤。量化层面:盈利因子 6.0,恢复因子 3.34;30 笔成交里 22 笔盈利,胜率 73.3%;平均盈利超过平均亏损 2 倍,单笔最大盈利是最大亏损的 3.5 倍。 有个怪现象值得盯——EA 只做买入、完全没碰卖出,平仓过程也没见深回撤。空仓卖单的原因原文没给结论,属于待挖的点。外汇品种高杠杆叠加单边样本,这类结果的外推性本就偏弱。 样本期偏短,上面这些数字只能算有苗头。要确认算法不是过拟合,得拉长历史窗口重跑几轮再说话。
「从回测短窗到真实盘的过渡」
Go-Explore 把记忆与重访有前途状态作为核心,不直接求解目标,而是先找相关状态-动作对。我们在 MT5 里顺次挂了三个 EA,各管一摊算法功能,共用同一套策略政策学习。 策略测试器里短时段跑出了较好结果之一,但窗口太短不具备代表性。外汇与贵金属杠杆高、滑点跳空频繁,实盘前务必拉长样本周期重训模型。 直接拿短窗回测 EA 上真实账户,大概率过拟合。先开 MT5 用三年以上 tick 数据复跑,观察回撤曲线再决定是否跟。
顺手可啃的几篇进阶材料
想把强化学习真正接进 MT5 策略,光看指标不够,得补一点智能体探索的底子。下面几篇原版技术文把难点拆得比较实在:Go-Explore 专治稀疏奖励下的艰难探索;第三十五、三十六部分分别讲了内在好奇心模块和关系强化学习,解决 agent 不知道「往哪试」的问题;第三十七、三十八部分则落到分散关注度与凭预测分歧做自我监督探索。 这些材料没有直接给 EA 代码,但第三十八部分提到的「用模型间分歧度当探索奖励」思路,可以直接搬到你现有网格或突破策略里,把原本随机加仓改成按分歧阈值触发。外汇与贵金属杠杆高,任何自监督探索逻辑都先在策略测试器跑 3 个月以上 tick 数据再上实盘。 真要验证,开 MT5 → 打开 MetaEditor → 新建一个空 EA,把分歧计算写成独立函数,日线级先回测 EURUSD 2020—2023,看触发频率是否落在每千根 5~20 次区间,偏离太多说明超参没调住。
◍ 随包附带的代码清单
这套 LSTM 优化策略不是只给了一段伪代码,作者把完整工程打进了 MQL5.zip(806.88 KB),里面分三层:两个实盘阶段的 EA(Faza1.mq5、Faza2.mq5)加一个政策微调后的 GE-lerning.mq5,另有四个支撑类库——Cell.mqh 管状态结构,FQF.mqh 组织完全参数化模型,NeuroNet.mqh 建神经网络,NeuroNet.cl 走 OpenCL 加速。
直接把 zip 解到 MT5 的 MQL5 根目录就能编译,不用自己拼依赖。但有读者在 2023.05.07 跑 Faza1 回测时撞到过 runtime error 502:OnTester 里数组越界(line 223),说明 action_count 下标没守住边界,开 MT5 把那行 Base[action_count].total_actions = action_count+StartCell.total_actions; 前后加个容量判断就能排掉。
外汇与贵金属杠杆高、滑点跳空频繁,这类神经网络 EA 在样本外容易漂移;上面那位作者自己也提醒过,若训练误差不降反升就调低学习系数。代码能跑通只是第一步,拿历史数据(比如 2023 年 4 月整月)先过一遍再上模拟盘更稳。