自训练EA基础篇:用遗传算法让策略自己再优化(基础篇)
🤖

自训练EA基础篇:用遗传算法让策略自己再优化(基础篇)

(1/3)·手动反复跑策略测试器选参数太笨,EA能否自己决定何时再优化并自我训练?

实战向 第 1/3 篇
多数交易者把EA写好就丢进策略测试器反复手调参数,市场一变又得停掉重跑。其实EA完全能在不中断交易的前提下,自己判断该不该再优化、并用遗传算法挑新参数。本篇先铺概念和框架,让你看懂自训练EA的骨架。

让 EA 自己学会交易

  • 年 3 月 7 日,Roman Zamozhnyy 在 MetaTrader 5 社区发布了一篇被阅读 3876 次、引发 15 条讨论的实战文章,核心思路是用自训练机制替代人工写死规则。作者借「Dr. Tradelove」这个玩笑代号,说明他不再纠结于主观择时,而是让程序从行情里自己提炼模式。

这套思路对外汇与贵金属交易者最直接的启发是:MT5 不只是跑固定策略的容器,你可以用历史数据喂给 EA,让它动态调整参数。高杠杆的外汇/贵金属市场里,任何自训练模型都只是概率工具,实盘前必须用至少一年的 tick 数据回测。 想验证可行性,开 MT5 新建一个 EA 骨架,把训练循环接进 OnTick,先拿 EURUSD 的 M15 历史跑一遍,看样本外表现再决定是否上模拟盘。

◍ 把再优化交给 EA 自己跑

写完 EA 后,大家习惯用 MT5 内置策略测试器反复跑优化,一有大的参数漂移就手动停掉重跑。这种人工中断的节奏,在实盘里根本跟不上。 Quantum 提出过一种思路:让真实账户跑一个系统,同时挂若干个(数量不限)虚拟策略副本,按当前盈利最大者自动切换,超过固定柱数就触发换策略。这等于把“什么时候该再优化”做成进程内决策,而不是人盯着测试器。 我更倾向直接用 joo 公开的遗传算法(GA)代码做底层。下面这段就是 2011 年自动交易锦标赛某参赛 EA 里的 GA 实施骨架,复制进 MT5 能直接看它怎么在运行时挑参数。 外汇与贵金属杠杆高,这类自适应 EA 只是降低人工中断频率,不消除回撤风险,上真仓前务必用策略测试器跑足样本。

「EA 再优化与适应度函数的落地骨架」

自优化 EA 的核心任务就三件:按选定策略交易、判断是否该再优化、用遗传算法(GA)跑出新的输入参数。最基础的再优化只换参数,进阶玩法才是让 GA 在变了形的行情里挑策略。 为了适配度函数好算,先定死一条:单品种只交易已收盘的柱子,不加仓、不部分平仓。适应度函数里我默认模拟了测试器的“Open Prices Only”模式,但这绝非唯一选项;想更严谨的人应当用“Every Tick”重做一遍模拟,否则止损止盈的回测结果可能失真。 建仓函数里有三个关键东西:s(GA 选的交易品种)、optF(GA 选的保证金占用比例)、GetPossibleLots()(返回实际手数)。另有两个全局函数,一个抓账户历史最大余额,一个算当前相对亏损,这俩是第二阶段再优化决策的锚。 初版先不玩多策略切换,实两个硬策略:双均线金叉买/死叉卖,以及接收近 5 根 K 线归一化涨跌幅的简单神经网络。EA 启动先初始化指标缓存或网络拓扑,然后直接调 GA 去解适应度函数(FF)。 每条策略有独立的 FF,不能复用。我的 FF 目标是最大化余额,前提是相对亏损不破外部变量阈值 0.5;若某次 GA 跑出余额 100000 但相对亏损 -0.6,FF 直接记 0.0。你完全可以换标准。 再优化触发线设在相对余额亏损 0.2。为避免触线后每根柱都重跑 GA,一旦触发就把“最大余额”刷新为当前值,作为下一轮起点。 下面这段是判断新柱开盘的辅助函数,M1 到 M30 比分钟位、H1 及以上比小时位,返回 true 才允许 EA 动手。 [CODE] 标记里的代码逐行拆:CopyTime 取当前品种 s 在周期 tf 的最新 1 根时间;TimeToStruct 转成结构体能拿分钟/小时;如果是 M 系周期且分钟数变了,就更新 prevBT 并返回 true,代表新柱;H 系周期逻辑对称未截完。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Define whether a new bar has opened&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">bool</span> isNewBars()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">CopyTime</span>(s,tf,<span class="number">class="num">0</span>,<span class="number">class="num">1</span>,curBT);
&nbsp;&nbsp; <span class="functions">TimeToStruct</span>(curBT[<span class="number">class="num">0</span>],curT);
&nbsp;&nbsp; <span class="keyword">if</span>(tf==<span class="keyword">PERIOD_M1</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M2</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M3</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M4</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M5</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M6</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M10</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M12</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M15</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M20</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_M30</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(curT.min!=prevT.min)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prevBT[<span class="number">class="num">0</span>]=curBT[<span class="number">class="num">0</span>];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">TimeToStruct</span>(prevBT[<span class="number">class="num">0</span>],prevT);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="keyword">true</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
&nbsp;&nbsp; <span class="keyword">if</span>(tf==<span class="keyword">PERIOD_H1</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_H2</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_H3</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_H4</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_H6</span>||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tf==<span class="keyword">PERIOD_H8</span>||

平仓逻辑里的执行模式分支

这段代码片段处理两个独立动作:其一是通过时间结构体比对判断当前 K 线相对前一根是否跨周期边界(M12 看 hour、D1/W1 看 day、MN1 看 mon),跨边界就把 prevBT[0] 刷新为 curBT[0] 并回 true;其二是 ClosePosition() 里根据品种执行模式决定下单参数。 在 SYMBOL_TRADE_EXECUTION_REQUEST 或 SYMBOL_TRADE_EXECUTION_INSTANT 模式下,代码把 sl/tp 置为 NULL、deviation 写死 100 点,说明这类账户不允许挂止损止盈随市价单一起发,只能后续改单补。 平仓循环用 while(PositionsTotal()>0) 而非 for,每轮取 MathMin(持仓量, SYMBOL_VOLUME_MAX) 再做 NormalizeDouble(…,2) 截断到 0.01 精度,避免超最大允许手数被拒单;卖单取 BID、买单取 ASK,价格随执行模式实时抓。 外汇与贵金属属高杠杆品种,REQUEST/INSTANT 模式下硬清 sl/tp 会带来裸奔风险,实盘前应在 MT5 用策略测试器切对应品种验证拒单率。

MQL5 / C++
   if(tf==PERIOD_M12)
      if(curT.hour!=prevT.hour)
         {
          prevBT[class="num">0]=curBT[class="num">0];
          TimeToStruct(prevBT[class="num">0],prevT);
          class="kw">return(true);
         };
   if(tf==PERIOD_D1||
      tf==PERIOD_W1)
      if(curT.day!=prevT.day)
         {
          prevBT[class="num">0]=curBT[class="num">0];
          TimeToStruct(prevBT[class="num">0],prevT);
          class="kw">return(true);
         };
   if(tf==PERIOD_MN1)
      if(curT.mon!=prevT.mon)
         {
          prevBT[class="num">0]=curBT[class="num">0];
          TimeToStruct(prevBT[class="num">0],prevT);
          class="kw">return(true);
         };
   class="kw">return(false);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  ClosePosition                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ClosePosition()
  {
   request.action=TRADE_ACTION_DEAL;
   request.symbol=PositionGetSymbol(class="num">0);
   if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) request.type=ORDER_TYPE_SELL; 
   else request.type=ORDER_TYPE_BUY;
   request.type_filling=ORDER_FILLING_FOK;
   if(SymbolInfoInteger(PositionGetSymbol(class="num">0),SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_REQUEST||
      SymbolInfoInteger(PositionGetSymbol(class="num">0),SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_INSTANT)
     {
      request.sl=NULL;
      request.tp=NULL;
      request.deviation=class="num">100;
     }
   while(PositionsTotal()>class="num">0)
     {
      request.volume=NormalizeDouble(MathMin(PositionGetDouble(POSITION_VOLUME),SymbolInfoDouble(PositionGetSymbol(class="num">0),SYMBOL_VOLUME_MAX)),class="num">2);
      if(SymbolInfoInteger(PositionGetSymbol(class="num">0),SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_REQUEST||
         SymbolInfoInteger(PositionGetSymbol(class="num">0),SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_INSTANT)
        {
         if(request.type==ORDER_TYPE_SELL) request.price=SymbolInfoDouble(s,SYMBOL_BID);
         else request.price=SymbolInfoDouble(s,SYMBOL_ASK);
        }

◍ 成交执行与手数补齐的底层写法

这段 OpenPosition 函数把下单逻辑和成交模式判断绑死了:当品种是 REQUEST 或 INSTANT 执行模式时,SL/TP 必须置 NULL、deviation 给到 100 点,否则市价单会被服务器拒掉。外汇和贵金属市场点差跳变频繁,这类限制在高波动时段可能直接导致发单失败。 手数不是拍脑袋定的。代码用空闲保证金乘以 optF 风险系数,再乘杠杆、除合约规模和最小步长,最后向下取整到 volume_step 的整数倍;随后用 MathMax 兜住最小交易量,再乘 0.95 的 GetPossibleLots 上限防超载。若品种有 SYMBOL_VOLUME_LIMIT,还会 NormalizeDouble 到 2 位小数并卡住最大值。 真正麻烦的是循环补齐。PositionSelect 没选到仓就按执行模式刷新买价卖价、OrderSend 后 Sleep(10000) 再查;仓位小于目标 vol 时继续拆单补,直到够数。这种 10 秒轮询在 EA 回测里不显形,实盘跑 XAUUSD 这类滑点大的品种可能拖慢建仓节奏。 把这段直接贴进 MT5 的 EA 源码,把 optF 从 0.1 调到 0.02 对比一下占用保证金,能直观看到爆仓距离被拉远多少。高风险品种请先在策略测试器用tick数据跑一遍。

MQL5 / C++
class="type">void OpenPosition()
  {
   class="type">class="kw">double vol;
   request.action=TRADE_ACTION_DEAL;
   request.symbol=s;
   request.type_filling=ORDER_FILLING_FOK;
   if(SymbolInfoInteger(s,SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_REQUEST||
      SymbolInfoInteger(s,SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_INSTANT)
     {
      request.sl=NULL;
      request.tp=NULL;
      request.deviation=class="num">100;
     }
   vol=MathFloor(AccountInfoDouble(ACCOUNT_FREEMARGIN)*optF*AccountInfoInteger(ACCOUNT_LEVERAGE)
/(SymbolInfoDouble(s,SYMBOL_TRADE_CONTRACT_SIZE)*SymbolInfoDouble(s,SYMBOL_VOLUME_STEP)))*SymbolInfoDouble(s,SYMBOL_VOLUME_STEP);
   vol=MathMax(vol,SymbolInfoDouble(s,SYMBOL_VOLUME_MIN));
   vol=MathMin(vol,GetPossibleLots()*class="num">0.95);
   if(SymbolInfoDouble(s,SYMBOL_VOLUME_LIMIT)!=class="num">0) vol=NormalizeDouble(MathMin(vol,SymbolInfoDouble(s,SYMBOL_VOLUME_LIMIT)),class="num">2);
   request.volume=NormalizeDouble(MathMin(vol,SymbolInfoDouble(s,SYMBOL_VOLUME_MAX)),class="num">2);
   while(PositionSelect(s)==false)
     {
      if(SymbolInfoInteger(s,SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_REQUEST||
         SymbolInfoInteger(s,SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_INSTANT)
        {
         if(request.type==ORDER_TYPE_SELL) request.price=SymbolInfoDouble(s,SYMBOL_BID);
else request.price=SymbolInfoDouble(s,SYMBOL_ASK);
        }
      OrderSend(request,result);
      Sleep(class="num">10000);
      PositionSelect(s);
     }
   while(PositionGetDouble(POSITION_VOLUME)<vol)
     {
      request.volume=NormalizeDouble(MathMin(vol-PositionGetDouble(POSITION_VOLUME),SymbolInfoDouble(s,SYMBOL_VOLUME_MAX)),class="num">2);
      if(SymbolInfoInteger(PositionGetSymbol(class="num">0),SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_REQUEST||
         SymbolInfoInteger(PositionGetSymbol(class="num">0),SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_INSTANT)
        {
把再优化决策交给小布盯盘
这些诊断与再优化触发逻辑,小布盯盘的AIGC已内置,打开对应品种页即可看到信号提示,你只需专注是否放行EA自训练。

常见问题

普通EA参数固定或手动优化,自训练EA能在运行中自行决定是否再优化并用算法重选参数,减少人为中断。
它基于已有代码搜索更优参数组合或策略,适应变化后的市场,而非单次暴力遍历全部可能。
对已完成柱交易够用;若要用止损获利或跟踪止损,需按现有算法在适应度函数里模拟Every Tick。
小布可监控品种波动与参数衰减,提示再优化窗口,省去你盯测试器的重复劳动。
为降低复杂度并聚焦再优化框架,限定单工具已完成柱交易,不涉及加仓与部分平仓。