Dr. Tradelove 或我如何不再担忧并创建一个自训练 EA 交易·进阶篇
📘

Dr. Tradelove 或我如何不再担忧并创建一个自训练 EA 交易·进阶篇

第 2/3 篇

用空闲保证金倒推可下手数

EA 里最容易被忽略的一步,是根据账户当前空闲保证金反算能开多大仓位,而不是写死 volume。下面这段 GetPossibleLots 就是干这个的:先把请求手数暂设为 1.0,按买卖方向取 BID 或 ASK 填充价格,再用 OrderCheck 拿到这笔 1 手占用的保证金 check.margin。 最后用空闲保证金除以单手保证金,NormalizeDouble 到 2 位小数返回。你在 MT5 里把这段贴进 EA,改一下返回精度,就能直观看到「当前环境最多扛几手」——外汇和贵金属杠杆浮动时,这个数每小时都可能变。 相对回撤的统计藏在 InitRelDD 和 GetRelDD 里。InitRelDD 从 2000 年拉一遍历史成交,把每笔 profit 累加到余额上,顺手记录历史最高余额 maxBalance;GetRelDD 则用 (maxBalance-当前余额)/maxBalance 给出相对回撤比例,数值落在 0~1 之间。 别把正态当圣经 这段初始化把基准时间写死成 D'2000.01.01 00:00:00',实盘换经纪商时历史可能没这么长,HistorySelect 返回空不会报错但 maxBalance 就只剩当前余额,回撤计算会失真。开 MT5 跑一遍,确认自己的账户历史起点再改这个常量。

MQL5 / C++
class="type">class="kw">double GetPossibleLots()
  {
   request.volume=class="num">1.0;
   if(request.type==ORDER_TYPE_SELL) request.price=SymbolInfoDouble(s,SYMBOL_BID);
   else request.price=SymbolInfoDouble(s,SYMBOL_ASK);
   OrderCheck(request,check);
   class="kw">return(NormalizeDouble(AccountInfoDouble(ACCOUNT_FREEMARGIN)/check.margin,class="num">2));
  }
class="type">void InitRelDD()
  {
   class="type">ulong DealTicket;
   class="type">class="kw">double curBalance;
   prevBT[class="num">0]=D&class="macro">#x27;class="num">2000.01.class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27;;
   TimeToStruct(prevBT[class="num">0],prevT);
   curBalance=AccountInfoDouble(ACCOUNT_BALANCE);
   maxBalance=curBalance;
   HistorySelect(D&class="macro">#x27;class="num">2000.01.class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27;,TimeCurrent());
   for(class="type">int i=HistoryDealsTotal();i>class="num">0;i--)
     {
      DealTicket=HistoryDealGetTicket(i);
      curBalance=curBalance+HistoryDealGetDouble(DealTicket,DEAL_PROFIT);
      if(curBalance>maxBalance) maxBalance=curBalance;
     }
  }
class="type">class="kw">double GetRelDD()
  {
   if(AccountInfoDouble(ACCOUNT_BALANCE)>maxBalance) maxBalance=AccountInfoDouble(ACCOUNT_BALANCE);
   class="kw">return((maxBalance-AccountInfoDouble(ACCOUNT_BALANCE))/maxBalance);
  }

◍ 双均线交叉与回撤触发的再优化逻辑

这段初始化把历史深度 depth 写死成 10000 根 K 线、单次拷贝 count 设成 2,意味着遗传优化只基于万根历史与最近两根均线值做决策,参数若不动,EA 在 MT5 回测里就会按这个窗口跑。 OnTick 里先靠 isNewBars() 拦截新柱,再用 CopyBuffer 把短、长均线各取 2 个点进 ShortBuffer / LongBuffer。多头翻转条件是 LongBuffer[0]>LongBuffer[1] 且 ShortBuffer[0]>LongBuffer[0] 且 ShortBuffer[1]<LongBuffer[1]——即长均线走平向上、短均线从下穿上并确认。 若已有多单且触发翻转,ClosePosition() 平掉并置 trig=true;空头对称处理。trig 成立后再查 GetRelDD()>maxDD,也就是实时回撤超阈值才调用 GA() 重新跑神经网络遗传优化。外汇与贵金属杠杆高,这种自动重优化可能频繁交易,实盘前务必用策略测试器验证触发频率。

MQL5 / C++
  TimeToStruct(prevBT[class="num">0],prevT);
class=class="str">"cmt">//--- historical depth(should be set since the optimisation is based on historical data)
  depth=class="num">10000;
class=class="str">"cmt">//--- copies at a time(should be set since the optimisation is based on historical data)
  count=class="num">2;
  ArrayResize(LongBuffer,count);
  ArrayResize(ShortBuffer,count);
  ArrayInitialize(LongBuffer,class="num">0);
  ArrayInitialize(ShortBuffer,class="num">0);
class=class="str">"cmt">//--- calling the neural network genetic optimisation function
  GA();
class=class="str">"cmt">//--- getting the optimised neural network parameters and other variables
  GetTrainResults();
class=class="str">"cmt">//--- getting the account drawdown
  InitRelDD();
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
  if(isNewBars()==true)
    {
    class="type">bool trig=false;
    CopyBuffer(MAshort,class="num">0,class="num">0,count,ShortBuffer);
    CopyBuffer(MAlong,class="num">0,class="num">0,count,LongBuffer);
    if(LongBuffer[class="num">0]>LongBuffer[class="num">1] && ShortBuffer[class="num">0]>LongBuffer[class="num">0] && ShortBuffer[class="num">1]<LongBuffer[class="num">1])
      {
      if(PositionsTotal()>class="num">0)
        {
        if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
          {
          ClosePosition();
          trig=true;
          }
        }
      }
    if(LongBuffer[class="num">0]<LongBuffer[class="num">1] && ShortBuffer[class="num">0]<LongBuffer[class="num">0] && ShortBuffer[class="num">1]>LongBuffer[class="num">1])
      {
      if(PositionsTotal()>class="num">0)
        {
        if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
          {
          ClosePosition();
          trig=true;
          }
        }
      }
    if(trig==true)
      {
      class=class="str">"cmt">//--- if the account drawdown has exceeded the allowable value:
      if(GetRelDD()>maxDD)
        {
        class=class="str">"cmt">//--- calling the neural network genetic optimisation function
        GA();

「均线交叉触发与遗传优化器参数」

双均线通道的入场判定直接写在缓冲区比较里:当长期均线走平向上(LongBuffer[0]>LongBuffer[1])、短期均线已上穿长期均线且前一根还未穿(ShortBuffer[0]>LongBuffer[0] 且 ShortBuffer[1]<LongBuffer[1])时,挂空单;反向条件对称则挂多单。外汇与贵金属杠杆高,这种交叉信号在历史回测中胜率随品种波动,实盘触发前建议在 MT5 策略测试器跑一遍 EURUSD 的 M15 数据。 遗传优化器 GA() 的群体规模由基因数决定:GeneCount = OptParamCount+2,染色体数固定为 GeneCount*11。搜索域锁死在 0.0–1.0,步长 0.0001,最大搜索模式 OptimizeMethod=2,连续 100 个 epoch 无改善才停。 UGA() 里七项概率分别是复制100%、自然突变1%、人工突变1%、基因借用1%、交叉1%、边界位移0.5%、单基因突变1%。想加快收敛就把 Epoch 降到 50,但过拟合风险会明显上升。

MQL5 / C++
class=class="str">"cmt">//--- getting the optimised neural network parameters and other variables
GetTrainResults();
class=class="str">"cmt">//--- readings of the drawdown will from now on be based on the current balance instead of the maximum balance
maxBalance=AccountInfoDouble(ACCOUNT_BALANCE);
 }
 }
 CopyBuffer(MAshort,class="num">0,class="num">0,count,ShortBuffer);
 CopyBuffer(MAlong,class="num">0,class="num">0,count,LongBuffer);
 if(LongBuffer[class="num">0]>LongBuffer[class="num">1] && ShortBuffer[class="num">0]>LongBuffer[class="num">0] && ShortBuffer[class="num">1]<LongBuffer[class="num">1])
  {
  request.type=ORDER_TYPE_SELL;
  OpenPosition();
  }
 if(LongBuffer[class="num">0]<LongBuffer[class="num">1] && ShortBuffer[class="num">0]<LongBuffer[class="num">0] && ShortBuffer[class="num">1]>LongBuffer[class="num">1])
  {
  request.type=ORDER_TYPE_BUY;
  OpenPosition();
  }
 };
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Preparing and calling the genetic optimizer               | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GA()
  {
class=class="str">"cmt">//--- number of genes(equal to the number of optimised variables), 
class=class="str">"cmt">//--- all of them should be specified in the FitnessFunction())
  GeneCount      =OptParamCount+class="num">2;    
class=class="str">"cmt">//--- number of chromosomes in a colony
  ChromosomeCount=GeneCount*class="num">11;
class=class="str">"cmt">//--- minimum search range
  RangeMinimum   =class="num">0.0;
class=class="str">"cmt">//--- maximum search range
  RangeMaximum   =class="num">1.0;
class=class="str">"cmt">//--- search pitch
  Precision      =class="num">0.0001;
class=class="str">"cmt">//--- class="num">1 is a minimum, anything else is a maximum
  OptimizeMethod =class="num">2;                                       
  ArrayResize(Chromosome,GeneCount+class="num">1);
  ArrayInitialize(Chromosome,class="num">0);
class=class="str">"cmt">//--- number of epochs without any improvement
  Epoch          =class="num">100;                                     
class=class="str">"cmt">//--- ratio of replication, natural mutation, artificial mutation, gene borrowing, 
class=class="str">"cmt">//--- crossingover, interval boundary displacement ratio, every gene mutation probabilty, %
  UGA(class="num">100.0,class="num">1.0,class="num">1.0,class="num">1.0,class="num">1.0,class="num">0.5,class="num">1.0);                              
  }

遗传算法里怎么挑货币对和均线周期

在 MT5 用遗传算法跑 EA 优化时,染色体最后一列基因被用来选交易品种,映射逻辑是把该基因值乘以 12 后四舍五入,得到 0~12 的整数再 switch 到具体符号。注意 case 0 和 case 1 都指向 AUDUSD,等于变相提高了澳美被选中的概率,这种冗余映射在回测里可能让品种分布失真。 短长均线周期则由前两根基因决定:MAshort 取 Colony[1][chromos]*MaxMAPeriod 四舍五入加 1,MAlong 取 Colony[2][chromos]*MaxMAPeriod 加 1,都走 iMA 的 MODE_SMA + PRICE_OPEN。dig 用 SymbolInfoInteger 拿品种小数位再算 10 的幂,后续处理价格精度会用到。 别把正态当圣经 品种选择写死在代码里意味着算法自由度和你预设的映射强相关。改 case 分支或调整乘數,回测中品种偏好可能完全变样,外汇与贵金属杠杆高、滑点随机,实盘前务必在策略测试器里重跑几代基因验证分布。

MQL5 / C++
class=class="str">"cmt">//| anything can be optimised but it is necessary                                        |
class=class="str">"cmt">//| to carefully monitor the number of genes                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FitnessFunction(class="type">int chromos)
  {
   class="type">int    b;
class=class="str">"cmt">//--- is there an open position?
   class="type">bool   trig=false;
class=class="str">"cmt">//--- direction of an open position
   class="type">class="kw">string dir="";
class=class="str">"cmt">//--- opening price
   class="type">class="kw">double OpenPrice=class="num">0;
class=class="str">"cmt">//--- intermediary between a gene colony and optimised parameters
   class="type">int    z;
class=class="str">"cmt">//--- current balance
   class="type">class="kw">double t=cap;
class=class="str">"cmt">//--- maximum balance
   class="type">class="kw">double maxt=t;
class=class="str">"cmt">//--- absolute drawdown
   class="type">class="kw">double aDD=class="num">0;
class=class="str">"cmt">//--- relative drawdown
   class="type">class="kw">double rDD=class="num">0.000001;
class=class="str">"cmt">//--- fitness function proper
   class="type">class="kw">double ff=class="num">0;
class=class="str">"cmt">//--- GA is selecting a pair
   z=(class="type">int)MathRound(Colony[GeneCount-class="num">1][chromos]*class="num">12);
   class="kw">switch(z)
     {
      case  class="num">0: {s="AUDUSD"; class="kw">break;};
      case  class="num">1: {s="AUDUSD"; class="kw">break;};
      case  class="num">2: {s="EURAUD"; class="kw">break;};
      case  class="num">3: {s="EURCHF"; class="kw">break;};
      case  class="num">4: {s="EURGBP"; class="kw">break;};
      case  class="num">5: {s="EURJPY"; class="kw">break;};
      case  class="num">6: {s="EURUSD"; class="kw">break;};
      case  class="num">7: {s="GBPCHF"; class="kw">break;};
      case  class="num">8: {s="GBPJPY"; class="kw">break;};
      case  class="num">9: {s="GBPUSD"; class="kw">break;};
      case class="num">10: {s="USDCAD"; class="kw">break;};
      case class="num">11: {s="USDCHF"; class="kw">break;};
      case class="num">12: {s="USDJPY"; class="kw">break;};
      class="kw">default: {s="EURUSD"; class="kw">break;};
     }
   MAshort=iMA(s,tf,(class="type">int)MathRound(Colony[class="num">1][chromos]*MaxMAPeriod)+class="num">1,class="num">0,MODE_SMA,PRICE_OPEN);
   MAlong =iMA(s,tf,(class="type">int)MathRound(Colony[class="num">2][chromos]*MaxMAPeriod)+class="num">1,class="num">0,MODE_SMA,PRICE_OPEN);
   dig=MathPow(class="num">10.0,(class="type">class="kw">double)SymbolInfoInteger(s,SYMBOL_DIGITS));

class=class="str">"cmt">//--- GA is selecting the optimal F

◍ 双均线交叉的历史回测记账逻辑

这段逻辑把遗传算法挑出的 optF(最优适应度)和账户杠杆、合约大小一起抓出来,作为历史回测的盈亏计算基底。leverage 取自 AccountInfoInteger(ACCOUNT_LEVERAGE),contractSize 用 SymbolInfoDouble 拿 SYMBOL_TRADE_CONTRACT_SIZE,外汇与贵金属品种这两值差异很大,回测前必须确认。 核心循环从 b 根历史 bar 往前扫,b 由 Bars(s,tf)-1-count-MaxMAPeriod 与 depth 取小得到,保证均线缓冲不越界。每一根都先 CopyBuffer 把长短均线最近 count 根拷进来,再用 LongBuffer[0]>LongBuffer[1] 且 ShortBuffer[0]>LongBuffer[0] 且 ShortBuffer[1]<LongBuffer[1] 判定短上穿长的做空触发。 触发后若 trig 为 false 就记 OpenPrice=o[1]、dir="SELL" 并置 trig;若已有多头持仓则平多转空,用 t=t+t*optF*leverage*(o[1]-OpenPrice)*dig/contractSize 累加净值,同时跟踪 maxt 与 aDD,rDD 取最大回撤比例。反向 LongBuffer[0]<LongBuffer[1] 且短下穿长则对称处理 BUY。 直接在 MT5 策略测试器里把这段嵌进 EA 的 OnTester 类函数,把 depth 设成 5000、count 设成 30,跑 EURUSD 的 H1 就可能看到 rDD 落在 0.2~0.4 区间;杠杆若从 100 调到 500,t 的波动幅度会明显放大,但最大回撤比例未必线性增长,值得手动验证。

MQL5 / C++
optF=Colony[GeneCount][chromos];
leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
contractSize=SymbolInfoDouble(s,SYMBOL_TRADE_CONTRACT_SIZE);
b=MathMin(Bars(s,tf)-class="num">1-count-MaxMAPeriod,depth);
class=class="str">"cmt">//--- for a neural network using historical data - where the data is copied from
for(from=b;from>=class="num">1;from--)
  {
   CopyBuffer(MAshort,class="num">0,from,count,ShortBuffer);
   CopyBuffer(MAlong,class="num">0,from,count,LongBuffer);
   if(LongBuffer[class="num">0]>LongBuffer[class="num">1] && ShortBuffer[class="num">0]>LongBuffer[class="num">0] && ShortBuffer[class="num">1]<LongBuffer[class="num">1])
     {
      if(trig==false)
        {
         CopyOpen(s,tf,from,count,o);
         OpenPrice=o[class="num">1];
         dir="SELL";
         trig=true;
        }
      else
        {
         if(dir=="BUY")
           {
            CopyOpen(s,tf,from,count,o);
            if(t>class="num">0) t=t+t*optF*leverage*(o[class="num">1]-OpenPrice)*dig/contractSize; else t=class="num">0;
            if(t>maxt) {maxt=t; aDD=class="num">0;} else if((maxt-t)>aDD) aDD=maxt-t;
            if((maxt>class="num">0) && (aDD/maxt>rDD)) rDD=aDD/maxt;
            OpenPrice=o[class="num">1];
            dir="SELL";
            trig=true;
           }
        }
     }
   if(LongBuffer[class="num">0]<LongBuffer[class="num">1] && ShortBuffer[class="num">0]<LongBuffer[class="num">0] && ShortBuffer[class="num">1]>LongBuffer[class="num">1])
     {
      if(trig==false)
        {
         CopyOpen(s,tf,from,count,o);
         OpenPrice=o[class="num">1];
         dir="BUY";
         trig=true;
        }
      else
        {
         if(dir=="SELL")
           {
            CopyOpen(s,tf,from,count,o);

常见问题

用空闲保证金除以单手占用保证金再乘安全系数(如0.5),得出最大可下手数,回测时按此上限封顶。
把回撤阈值设为近期ATR的1.5倍,且仅在收盘价确认交叉后第二根K线才进场,能过滤大部分毛刺。
可以,小布能按你给的均线和回撤规则自动回测、生成每笔交易盈亏台账,你直接看汇总概率就行。
先圈定流动性高的6个货币对,均线周期网格搜5~200,让优化器只跑这两维,省七成算力。
在出场平仓事件里写一次静态数组追加,记录开平时间、手数、点数差,别依赖账户历史订单防止重连丢数据。