Trademinator 3:交易机器的崛起·进阶篇
📘

Trademinator 3:交易机器的崛起·进阶篇

第 2/2 篇

◍ 适应度函数里的随机指标封装

这段逻辑把遗传算法里的适应度计算收了尾:先按策略分支调用 FFMA 拿 fitness 值,计数器 AmountStartsFF 自增,再把结果写回 Colony[0][chromos],最后用 Print 把时间、模块名和五个参数打到日志,方便你直接在 MT5 Experts 标签里抓每次迭代的染色体取值。 真正干活的是 FFStoch 函数。它先声明了持仓状态 FFtrig、方向 dir、开仓价 OpenPrice,以及当前资金 t、峰值 maxt、绝对回撤 aDD 和相对回撤 rDD(初始 0.000001 防除零)。外汇与贵金属波动剧烈,这类回撤跟踪必须带下限保护,否则后续比率计算会直接崩。 函数末尾用 iStochastic 建指标句柄,周期参数走 MathRound(par1*MaxStochPeriod)+1——意味着随机指标 K 周期被映射成 par1 乘最大周期再取整加一。你改 MaxStochPeriod 数值,就能让算法在更大范围内搜 Stoch 参数,实盘前建议先用策略测试器跑一遍看收敛速度。

MQL5 / C++
 Colony[class="num">4][chromos],
 Colony[class="num">5][chromos]); class="kw">break;};
 class="kw">default: {ff=FFMA( Colony[class="num">1][chromos],
 Colony[class="num">2][chromos],
 Colony[class="num">3][chromos],
 Colony[class="num">4][chromos],
 Colony[class="num">5][chromos]); class="kw">break;};
 }
 AmountStartsFF++;
 Colony[class="num">0][chromos]=ff;
 Print(TimeToString(TimeCurrent()),";","GAModule:FitnessFunction",
 ";","strat=",strat,";","s=",s,";","optF=",optF,
 ";",Colony[class="num">1][chromos],";",Colony[class="num">2][chromos],";",Colony[class="num">3][chromos],";",Colony[class="num">4][chromos],";",Colony[class="num">5][chromos]);
}
class="type">class="kw">double FFStoch(class="type">class="kw">double par1,class="type">class="kw">double par2,class="type">class="kw">double par3,class="type">class="kw">double par4,class="type">class="kw">double par5)
{
 class="type">int b;
 class="type">bool FFtrig=false; class=class="str">"cmt">//Is there an open position?
 class="type">class="kw">string dir=""; class=class="str">"cmt">//Direction of the open position
 class="type">class="kw">double OpenPrice; class=class="str">"cmt">//Position Open price
 class="type">class="kw">double t=cap; class=class="str">"cmt">//Current balance
 class="type">class="kw">double maxt=t; class=class="str">"cmt">//Maximum balance
 class="type">class="kw">double aDD=class="num">0.0; class=class="str">"cmt">//Absolute drawdown
 class="type">class="kw">double rDD=class="num">0.000001; class=class="str">"cmt">//Relative drawdown
 Stoch=iStochastic(s,tf,(class="type">int)MathRound(par1*MaxStochPeriod)+class="num">1,

用随机指标交叉回测权益曲线

这段逻辑把 Stochastic 的主线/信号线交叉当成触发事件,在历史区间里向前滚动复制缓冲,逐根 K 线重算一个模拟权益值 t。注意它并不是真下单,而是用杠杆、合约大小和开盘价差来近似每笔波动的权益增减,外汇和贵金属品种因高杠杆属性,这种近似对回测结果放大效应明显,实盘风险远高于纸面。 核心判定在第一层 if:当主线与信号线发生金叉或死叉时,若上一次触发标记 FFtrig 为真,就按方向用上一根开盘价 o[1] 与开仓价 OpenPrice 的差来更新 t。BUY 方向用 (o[1]-OpenPrice),SELL 方向反过来,再乘上 optF*leverage*dig/contractSize,dig 由 SYMBOL_DIGITS 通过 MathPow(10.0, digits) 算出,保证点位换算到报价精度。 回测里同时维护 maxt(峰值权益)和 aDD(绝对回撤),一旦 (maxt-t) 超过 aDD 就刷新;若 maxt>0 且 aDD/maxt 大于 rDD,则把 rDD 更新为当前回撤比例。也就是说 rDD 最终存的是这段历史里观测到的最大回撤率,是一个可直接拿去横向比较不同参数组的数据点。 触发开仓的方向由第二层 if 决定:只有死叉且前一根主线高于 StochTopLimit(par4*100.0)才记 dir="SELL" 并置 FFtrig=true,等待下一处交叉去结算。想验证就直接把下面代码贴进 MT5 脚本,改 par4 看 rDD 怎么变。

MQL5 / C++
  (class="type">int)MathRound(par2*MaxStochPeriod)+class="num">1,
  (class="type">int)MathRound(par3*MaxStochPeriod)+class="num">1,MODE_SMA,STO_CLOSECLOSE);
  StochTopLimit   =par4*class="num">100.0;
  StochBottomLimit=par5*class="num">100.0;
  dig=MathPow(class="num">10.0,(class="type">class="kw">double)SymbolInfoInteger(s,SYMBOL_DIGITS));
  leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
  contractSize=SymbolInfoDouble(s,SYMBOL_TRADE_CONTRACT_SIZE);
  b=MathMin(Bars(s,tf)-class="num">1-count-MaxMAPeriod,depth);
  for(from=b;from>=class="num">1;from--)                                                      class=class="str">"cmt">//Where to start copying of history
  {
   CopyBuffer(Stoch,class="num">0,from,count,StochBufferMain);
   CopyBuffer(Stoch,class="num">1,from,count,StochBufferSignal);
   if((StochBufferMain[class="num">0]>StochBufferSignal[class="num">0]&&StochBufferMain[class="num">1]<StochBufferSignal[class="num">1])||
       (StochBufferMain[class="num">0]<StochBufferSignal[class="num">0]&&StochBufferMain[class="num">1]>StochBufferSignal[class="num">1]))
   {
     if(FFtrig==true)
     {
       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;
       }
       if(dir=="SELL")
       {
         CopyOpen(s,tf,from,count,o);
         if(t>class="num">0) t=t+t*optF*leverage*(OpenPrice-o[class="num">1])*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;
       }
       FFtrig=false;
     }
   }
    if(StochBufferMain[class="num">0]>StochBufferSignal[class="num">0]&&StochBufferMain[class="num">1]<StochBufferSignal[class="num">1]&&StochBufferMain[class="num">1]>StochTopLimit)
    {
      CopyOpen(s,tf,from,count,o);
      OpenPrice=o[class="num">1];
      dir="SELL";
      FFtrig=true;
    }

「遗传算法里怎么锁死品种与手数」

这段逻辑出现在策略优化器的参数落库阶段:染色体末位基因决定交易标的,倒数第二位决定策略编号,末位之外的 optF 基因记录最优仓位占比。实盘和回测对品种的处理分两套写法,回测写死 EURUSD / GBPUSD / USDCHF / USDJPY 四选一,实盘则改为从 MarketWatch 已选品种里动态取 SymbolName(z,true)。 z=(int)MathRound(Chromosome[GeneCount-1]*3) 把连续基因映射到 0~3 整数,switch 里 default 兜底回 EURUSD,避免越界基因导致空品种。若你要在 MT5 里跑多品种遗传优化,把注释块里的 SymbolsTotal(true) 写法取消注释、删掉硬编四币种即可,否则永远只能测那四个直盘。 外汇与贵金属杠杆品种回测收益不代表实盘,遗传搜索出的高 optF 在样本外可能快速衰减,切换实盘前建议先用历史中心做 Out-of-sample 验证。

MQL5 / C++
class="type">void GetTrainResults()
{
  strat=(class="type">int)MathRound(Chromosome[GeneCount-class="num">2]*StratCount);
  z=(class="type">int)MathRound(Chromosome[GeneCount-class="num">1]*class="num">3);
  class="kw">switch(z)
  {
    case  class="num">0: {s="EURUSD"; class="kw">break;};
    case  class="num">1: {s="GBPUSD"; class="kw">break;};
    case  class="num">2: {s="USDCHF"; class="kw">break;};
    case  class="num">3: {s="USDJPY"; class="kw">break;};
    class="kw">default: {s="EURUSD"; class="kw">break;};
  }
  optF=Chromosome[GeneCount];
  class="kw">switch(strat)
  {
    case  class="num">0: {GTRMA( Chromosome[class="num">1],
                     Chromosome[class="num">2],
                     Chromosome[class="num">3],
                     Chromosome[class="num">4],

◍ 染色体如何映射到具体策略函数

遗传模块跑完一轮后,会把编码在 Chromosome 数组里的 5 个浮点参数,按 strat 分支派发到对应的策略函数。case 0 走 SAR、case 1 走 SAR 变体、case 2 走 Stochastic,其余一律回落到默认的移动均线逻辑,每个分支末尾都用 break 切断后续判断。 以 GTRMA 为例,par1 与 par2 会先乘上 MaxMAPeriod 再做 MathRound 取整,并强制 +1 作为均线周期。也就是说若 MaxMAPeriod=200、par1=0.05,实际短均线周期就是 11(0.05×200=10,+1 后取整)。这种映射把连续搜索空间压成了整数周期,避免 EA 去试 10.3 这类无意义参数。 NeedOpenMA 里只用 CopyBuffer 把最新 count 根 K 线的 ShortBuffer、LongBuffer 拉回本地,并不重算指标句柄。回测时若 from 与 count 设错,会出现缓冲区错位却无任何报错,建议开 MT5 用 Print 把数组首尾值打出来核对。外汇与贵金属杠杆高,遗传优化出的参数过拟合概率偏大,实盘前务必用 walk-forward 分段验证。

MQL5 / C++
case class="num">1: {GTRSAR( Chromosome[class="num">1],
                     Chromosome[class="num">2],
                     Chromosome[class="num">3],
                     Chromosome[class="num">4],
                     Chromosome[class="num">5]) ; class="kw">break;};
case class="num">2: {GTRStoch(Chromosome[class="num">1],
                  Chromosome[class="num">2],
                  Chromosome[class="num">3],
                  Chromosome[class="num">4],
                  Chromosome[class="num">5]) ; class="kw">break;};
class="kw">default: {GTRMA( Chromosome[class="num">1],
                Chromosome[class="num">2],
                Chromosome[class="num">3],
                Chromosome[class="num">4],
                Chromosome[class="num">5]) ; class="kw">break;};
}
Print(TimeToString(TimeCurrent()),";","GAModule:GetTrainResults",
      ";","strat=",strat,";","s=",s,";","optF=",optF,
      ";",Chromosome[class="num">1],";",Chromosome[class="num">2],";",Chromosome[class="num">3],";",Chromosome[class="num">4],";",Chromosome[class="num">5]);
}
class="type">void GTRMA(class="type">class="kw">double par1,class="type">class="kw">double par2,class="type">class="kw">double par3,class="type">class="kw">double par4,class="type">class="kw">double par5)
{
  MAshort=iMA(s,tf,(class="type">int)MathRound(par1*MaxMAPeriod)+class="num">1,class="num">0,MODE_SMA,PRICE_OPEN);
  MAlong =iMA(s,tf,(class="type">int)MathRound(par2*MaxMAPeriod)+class="num">1,class="num">0,MODE_SMA,PRICE_OPEN);
  CopyBuffer(MAshort,class="num">0,from,count,ShortBuffer);
  CopyBuffer(MAlong, class="num">0,from,count,LongBuffer );
  Print(TimeToString(TimeCurrent()),";","StrategyMA:GTRMA",
        ";","MAL=",(class="type">int)MathRound(par2*MaxMAPeriod)+class="num">1,";","MAS=",(class="type">int)MathRound(par1*MaxMAPeriod)+class="num">1);
}
class="type">bool NeedOpenMA()
{
  CopyBuffer(MAshort,class="num">0,class="num">0,count,ShortBuffer);
  CopyBuffer(MAlong, class="num">0,class="num">0,count,LongBuffer );

别急着下结论

上面这段逻辑把双缓冲交叉变成了可执行的市价单触发:当长线缓冲上移且短线缓冲从下往上击穿长线时,挂空单;反向则挂多单。直接在 MT5 的 EA 里把 LongBuffer、ShortBuffer 接上你自己的均线周期,Print 行能让你在专家日志里看到 LB[0]=、SB[1]= 这类实时数值,确认交叉是否成立。 外汇与贵金属杠杆高,这类缓冲交叉在震荡市会连续假突破,实盘前先用策略测试器跑至少 3 个月 Tick 数据看回撤。 参数没调好之前,宁可让 return(true) 空跑几根 K 线,也别一交叉就 OpenPosition。

MQL5 / C++
Print(TimeToString(TimeCurrent()),":","StrategyMA:NeedOpenMA",
					",","LB[class="num">0]=",LongBuffer[class="num">0],":","LB[class="num">1]=",LongBuffer[class="num">1],":","SB[class="num">0]=",ShortBuffer[class="num">0],":","SB[class="num">1]=",ShortBuffer[class="num">1]);
	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();
		class="kw">return(false);
	}
	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="kw">return(false);
	}
	class="kw">return(true);
}

常见问题

先固定品种和手数再跑,随机指标交叉在震荡市概率盈利,趋势市容易连续止损,别只看总收益。
把品种与手数写死在染色体映射层,不参与变异交叉,只让指标周期和阈值进化。
小布可对接你的回测结果页,标记样本内外的权益曲线背离,并提示随机指标参数是否落在极端区。
用枚举型基因限定取值范围,映射层做语法护栏,越界个体直接零适应度,不进下一代。
正常,每代都要重算指标缓冲;把指标句柄缓存到全局,品种锁死后速度能回来三成以上。