神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式·综合运用
📘

神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式·综合运用

第 3/3 篇

◍ 训练循环里的状态快照与收盘落盘

这段逻辑跑在 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 逐项入栈。

MQL5 / C++
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 尾段,能验证特征向量维度与网络输出是否对齐。

MQL5 / C++
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 用不同年份数据重跑验证。

MQL5 / C++
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 月整月)先过一遍再上模拟盘更稳。

常见问题

在每轮训练结束时把当前网络权重、环境状态和回放缓冲序列化保存,收盘时统一写入文件,避免中途崩溃丢失进度。
至少传入可用保证金、持仓方向、开仓均价和浮动盈亏,网络据此输出手数调整或平仓信号,再走风控校验下单。
小布可接管数据抓取、快照落盘与信号提醒,你只需在品种页开启对应策略,盘中异常它会推送到盯盘界面。
原文分探索、利用、微调三阶段各约数千根 bar,回测显示样本内倾向正期望,但换周期外推概率衰减,需实盘验证。
点差与滑点未计入回放、过度拟合短窗噪声是主因;建议先迷你仓跑两周,对齐成交延迟再放大敞口。