优化结果的可视化评估·综合运用
📊

优化结果的可视化评估·综合运用

(3/3)· 用独立包含文件把多自定义准则和余额/净值图塞进单次优化,省下反复跑测试的时间

案例拆解 第 3/3 篇
多数人会为不同过滤条件反复跑多轮优化,耗时不说还难横向比对。其实一次通测里就能顺手收集多组准则并画出权益曲线,只看默认报告等于丢掉了八成信息量。

「回测图上叠加回归线与统计文字的坑」

在 MT5 自定义回测图表里,余额与净值回归线通过 CurveAdd 分别挂成蓝、红两色,线宽由 widthL 控制;当选择 lineR_BalanceEquity 分支时,会同时加两条曲线,注意代码里 curve2.LinesWidth 被调了两次,curve3 的宽度其实没单独设,复制时容易漏。 图形绘制有个硬顺序:所有 LineAdd / TextAdd 必须放在 CurvePlotAll 之后,否则曲线会把后画的竖线和文字盖掉。源码注释直接标了 Important!!!,这不是客气话。 竖线用 ScaleX(size-1) 映射到 X 轴,坐标从 y=30 到 _height-45,标记的是回测样本末端;文字区从 (70,30) 起按 FontSet 行距排,系数按 i 值走不同精度:i=0/2 用 %.2f,i=1/3 用 %d,其余 %.4f。 最后两行把 STAT_PROFIT_FACTOR 和 STAT_EXPECTED_PAYOFF 写成绿色文字,例如 Profitability = 1.35、Expected payoff = 12.40 这类数字会直接落在图上,开 MT5 跑一遍就能看到实际值。外汇与贵金属回测高风险,数值仅反映历史样本,不代表未来。

MQL5 / C++
  CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
  curve2.LinesWidth(widthL);
  }
  class="kw">break;
  case   lineR_BalanceEquity: class=class="str">"cmt">// balance and equity regression line
    {
    size = ArraySize(LRegressB);
    CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
    curve2.LinesWidth(widthL);
    CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
    curve2.LinesWidth(widthL);
    }
  class="kw">break;
  class="kw">default:
    class="kw">break;
  }
class=class="str">"cmt">//--- plot curves
  graphic.CurvePlotAll();
class=class="str">"cmt">// Important!!!  All lines and captions must be created after creating the graph; otherwise, the graph will class="kw">override them
  if(size == class="num">0)
    {
    size = ArraySize(LRegressE);
    if(size == class="num">0)
      size = ArraySize(LRegressB);
    }
  class="type">int x1 = graphic.ScaleX(size - class="num">1); class=class="str">"cmt">//Scales the value of the number of trades along the X axis
  graphic.LineAdd(x1, class="num">30, x1, _height - class="num">45, ColorToARGB(clrBlue), LINE_END_BUTT); class=class="str">"cmt">// construct the vertical line denoting the end of the Backward period
  class="type">class="kw">string txt = "";
  class="type">int txt_x = class="num">70;class=class="str">"cmt">// text indent along the X axis
  class="type">int txt_y = class="num">30;class=class="str">"cmt">// text indent along the Y axis
  graphic.FontSet("Arial", FontSet);class=class="str">"cmt">// Set current font parameters
  for(class="type">int i = class="num">0; i < size_sort; i++)  class=class="str">"cmt">// Write all coefficients and criteria on the chart
    {
    if(coeff[i] == class="num">0)
      class="kw">continue;
    if(i == class="num">1 || i == class="num">3)
      txt = StringFormat("%s = %d", EnumToString((sort)i), (class="type">int)coeff[i]);
    else
      if(i == class="num">0 || i == class="num">2)
        txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]);
      else
        txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]);
    graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen));
    }
  txt_y = txt_y + FontSet * (size_sort - class="num">1);
  txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]);
  graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen));
  txt = StringFormat("Expected  payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]);
  graphic.TextAdd(txt_x, txt_y + FontSet * class="num">2, txt, ColorToARGB(clrGreen));
  graphic.Update();
class=class="str">"cmt">//--- class="kw">return resource name

◍ 把图表里的位图对象单独导成文件

想在 MT5 里把某个 OBJ_BITMAP 或 OBJ_BITMAP_LABEL 对象单独存成图片,直接对原图截图会带背景,上面这段逻辑的做法是:先建一个隐藏图表,把位图对象复制过去再截,背景就干净了。 函数开头若传入空对象名直接返回 true,不报错;接着用 ObjectGetInteger 取对象类型,只有位图类才继续。复制时给临时对象名拼了 MathRand(),避免与原对象重名冲突。 关键一步是把隐藏图表 CHART_SHOW 设为 false,再用 ChartScreenShot 按对象自身 XSIZE/YSIZE 截出文件。若 FullImage 为 false,还会把原对象的 XOFFSET/YOFFSET 一并设上,只截局部。外汇与贵金属行情波动剧烈,这类脚本仅用于离线分析,实盘前请在策略测试器充分验证。

MQL5 / C++
  class="kw">return graphic.ChartObjectName();
  }
class="type">bool BitmapObjectToFile(class="kw">const class="type">class="kw">string ObjName, class="kw">const class="type">class="kw">string _FileName, class="kw">const class="type">bool FullImage = true)
  {
   if(ObjName == "")
      class="kw">return(true);
   class="kw">const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(class="num">0, ObjName, OBJPROP_TYPE);
   class="type">bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP);
   if(Res)
     {
       class="kw">const class="type">class="kw">string Name = __FUNCTION__ + (class="type">class="kw">string)MathRand();
       ObjectCreate(class="num">0, Name, OBJ_CHART, class="num">0, class="num">0, class="num">0);
       ObjectSetInteger(class="num">0, Name, OBJPROP_XDISTANCE, -class="num">5e3);
       class="kw">const class="type">long chart = ObjectGetInteger(class="num">0, Name, OBJPROP_CHART_ID);
       Res = ChartSetInteger(chart, CHART_SHOW, class="kw">false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0) &&
             ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(class="num">0, ObjName, OBJPROP_BMPFILE)) &&
             (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(class="num">0, ObjName, OBJPROP_XSIZE)) &&
                   ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(class="num">0, ObjName, OBJPROP_YSIZE)) &&
                   ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(class="num">0, ObjName, OBJPROP_XOFFSET)) &&
                   ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(class="num">0, ObjName, OBJPROP_YOFFSET)))) &&
             ChartScreenShot(chart, FolderName + "\\" + _FileName, (class="type">int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (class="type">int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE));
       ObjectDelete(class="num">0, Name);
     }
   class="kw">return(Res);
  }

把回归线塞进优化器当裁判

自定义优化准则最好集中放在一个独立的 .mqh 包含文件里,这样画图的脚本和跑优化的 EA 都能直接调用同一套计算,不用各写一遍。实际落地时,我往项目里塞了四个准则:R^2 判定系数、ProfitStability(利润稳定性)、TSSF(交易系统安全系数)和 LinearFactor(线性系数),可惜没加上文献里提到的“复杂标准 max”,因为始终没翻到它的具体算法。 R^2 衡量的是余额或净值曲线贴近线性回归线的程度,值越接近 1 说明曲线越像一条稳定爬升的直线;ProfitStability 则用回归线终点减起点的 TrendProfit 除以 LR 标准误差,比值越高代表“趋势利润”相对回撤噪声越干净。TSSF 沿用 2011 年一篇老文中的安全系数公式:Avg.Win / Avg.Loss 乘上 ((110% - %Win) / (%Win - 10%) + 1),对胜率和盈亏比同时敏感。 LinearFactor 在某些 EA 上效果极好,算法是 MaxDeviation / EndBalance,其中 MaxDeviation 取每根余额与平均线偏离的绝对值最大值,平均线斜率 K = (EndBalance - StartBalance) / n,n 是测试成交数。作者私下提过这个准则还能再增强,但没在代码里实现。 要提醒的是,不存在一个通吃所有 EA 的通用准则——不同逻辑的策略换不同准则才可能给出更优的遗传优化结果。外汇与贵金属市场高杠杆、高波动,任何准则都只是过滤噪声的辅助,回测漂亮不代表实盘概率占优。 下面这段 Coeff 函数是四个准则的计算核心,用 AlgLib 做线性回归后顺手把 R^2 和 ProfitStability 都算了出来: void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c) { //-- Fill the matrix: Y - Array value, X - ordinal number of the value 把余额序列和序号填进矩阵,Y 是余额值,X 是次序 CMatrixDouble xy(total, 2); for(int i = 0; i < total; i++) { xy[i].Set(0, i); xy[i].Set(1, Array[i]); } //-- Find coefficients a and b of the linear model y = a*x + b; 用 AlgLib 求线性回归 y=a*x+b 的 a 和 b int retcode = 0; double a, b; CLinReg::LRLine(xy, total, retcode, b, a); //-- Generate the linear regression values for each X; 按求出的 a、b 生成每条 X 对应的回归线值 ArrayResize(LR, total); for(int x = 0; x < total; x++) LR[x] = x * a + b; if(m_calc == c) { //-- Find the coefficient of correlation of values with their linear regression 算序列与回归线的皮尔逊相关系数 corr = CAlglib::PearsonCorr2(Array, LR); //-- Find R^2 and its sign 平方得 R^2,并按首尾余额方向决定正负号 coeff[r2] = MathPow(corr, 2.0); int sign = 1; if(Array[0] > Array[total - 1]) sign = -1; coeff[r2] *= sign; //-- Find LR Standard Error 计算回归标准误差(自由度 total-2) if(total - 2 == 0) stand_err = 0; else { for(int i = 0; i < total; i++) { double delta = MathAbs(Array[i] - LR[i]); stand_err = stand_err + delta * delta; } stand_err = MathSqrt(stand_err / (total - 2)); } } //-- Find ProfitStability = Profit_LR/stand_err 趋势利润除以标准误差得到利润稳定性 if(stand_err == 0) coeff[ProfitStability] = 0; else coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err; }

if(TeSt[STAT_PROFIT_TRADES] == 0TeSt[STAT_LOSS_TRADES] == 0TeSt[STAT_TRADES] == 0)

coeff[TSSF] = 0; else {

MQL5 / C++
class="type">void Coeff(class="type">class="kw">double& Array[], class="type">class="kw">double& LR[], class="type">class="kw">double& coeff[], class="type">class="kw">double& TeSt[], class="type">int total, class="type">int c)
  {
class=class="str">"cmt">//-- Fill the matrix: Y - Array value, X - ordinal number of the value
  CMatrixDouble xy(total, class="num">2);
  for(class="type">int i = class="num">0; i < total; i++)
    {
      xy[i].Set(class="num">0, i);
      xy[i].Set(class="num">1, Array[i]);
    }
class=class="str">"cmt">//-- Find coefficients a and b of the linear model y = a*x + b;
  class="type">int retcode = class="num">0;
  class="type">class="kw">double a, b;
  CLinReg::LRLine(xy, total, retcode, b, a);
class=class="str">"cmt">//-- Generate the linear regression values for each X;
  ArrayResize(LR, total);
  for(class="type">int x = class="num">0; x < total; x++)
      LR[x] = x * a + b;
  if(m_calc == c)
    {
class=class="str">"cmt">//-- Find the coefficient of correlation of values with their linear regression
      corr = CAlglib::PearsonCorr2(Array, LR);
class=class="str">"cmt">//-- Find R^class="num">2 and its sign
      coeff[r2] = MathPow(corr, class="num">2.0);
      class="type">int sign = class="num">1;
      if(Array[class="num">0] > Array[total - class="num">1])
          sign = -class="num">1;
      coeff[r2] *= sign;
class=class="str">"cmt">//-- Find LR Standard Error
      if(total - class="num">2 == class="num">0)
          stand_err = class="num">0;
      else
        {
          for(class="type">int i = class="num">0; i < total; i++)
            {
              class="type">class="kw">double delta = MathAbs(Array[i] - LR[i]);
              stand_err =  stand_err + delta * delta;
            }
          stand_err = MathSqrt(stand_err / (total - class="num">2));
        }
    }
class=class="str">"cmt">//-- Find ProfitStability = Profit_LR/stand_err
  if(stand_err == class="num">0)
      coeff[ProfitStability] = class="num">0;
  else
      coeff[ProfitStability] = (LR[total - class="num">1] - LR[class="num">0]) / stand_err;
  }
  if(TeSt[STAT_PROFIT_TRADES] == class="num">0 || TeSt[STAT_LOSS_TRADES] == class="num">0 || TeSt[STAT_TRADES] == class="num">0)
      coeff[TSSF] = class="num">0;
  else
    {

「从回测统计里抠出稳健系数」

这段逻辑干的事,是把历史交易的盈亏分布压成两个可比对的比值:一个是理论安全线,一个是真实表现。先算平均盈利和平均亏损,再用盈利单占比反推一个「teor」理论盈亏比,最后拿真实 avg_win/avg_loss 去除以它,得到 TSSF 系数。 代码里有个硬边界:当 win_perc 落在 10% 附近使得分母为 0 时,直接把 coeff[TSSF] 置 0,避免除零崩脚本。实际跑下来,如果某 EA 的盈利单只有 12%,teor 会飙到 (110-12)/(12-10)+1 = 50,真实比若只有 2,那 TSSF 就只有 0.04,说明这策略的赔率结构极脆弱。 后半段换了个角度:用资金曲线做线性拟合,找最大偏离度。K 是整体斜率,MaxDeviaton 抓的是每根余额点与「起点+斜率×步数」这条理想线的最大绝对差。 LinearFactor 用 1/(MaxDeviaton/Balance[0]) 表达,偏离占初始资金比越小,系数越接近 1,曲线越像稳定爬坡。外汇与贵金属杠杆高,这类系数只反映历史样本,换周期可能失效,验证时务必在 MT5 策略测试器里多跑几个品种。

MQL5 / C++
class="type">class="kw">double avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES];
class="type">class="kw">double avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES];
class="type">class="kw">double win_perc = class="num">100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES];
class=class="str">"cmt">//  Calculate the secure ratio for this percentage of profitable deals:
if((win_perc - class="num">10.0) + class="num">1.0 == class="num">0)
   coeff[TSSF] = class="num">0;
else
   {
    class="type">class="kw">double  teor = (class="num">110.0 - win_perc) / (win_perc - class="num">10.0) + class="num">1.0;
    class=class="str">"cmt">//  Calculate the real ratio:
    class="type">class="kw">double  real = avg_win / avg_loss;
    if(teor != class="num">0)
        coeff[TSSF] = real / teor;
    else
        coeff[TSSF] = class="num">0;
   }
 }
 class="type">class="kw">double MaxDeviaton = class="num">0;
 class="type">class="kw">double K = (Balance[total - class="num">1] - Balance[class="num">0]) / total;
 for(class="type">int i = class="num">0; i < total; i++)
  {
   if(i == class="num">0)
     MaxDeviaton = MathAbs(Balance[i] - (Balance[class="num">0] + K * i));
   else
     if(MathAbs(Balance[i] - (Balance[class="num">0] + K * i) > MaxDeviaton))
       MaxDeviaton = MathAbs(Balance[i] - (Balance[class="num">0] + K * i));
  }
 if(MaxDeviaton ==class="num">0 || Balance[class="num">0] == class="num">0)
   coeff[LinearFactor] = class="num">0;
 else
   coeff[LinearFactor] = class="num">1 / (MaxDeviaton / Balance[class="num">0]);

◍ 自定义优化准则怎么挑参数

做 EA 优化时,最直观的目标是净值曲线贴近一条向右上的直线,但 MT5 自带的比较只能横向比同一 EA 的不同参数组。交易次数、开仓手数这类维度我基本忽略——一次赚上千和上百次累赚上千,在自定义评分里没区别;不过样本太少时线性回归本身会失真,这点要心里有数。 真正卡死的两项是相对盈利和回撤。相对盈利用净利润除以初始保证金,回撤只看低于余额的最大净值落差,因为高于余额的浮赢根本没落袋,不疼;低于余额的回撤才是实打实的伤。自定义值按升序排,所以从基准里扣掉回撤,数值越高说明回撤越小。 原先的净值采集会漏掉最低点,后来改用两个变量:亏损结束时存最大净值、盈利结束时存最小净值,另一路只存最小净值,采集方式写进 TeSt[41] 交给 equityDD() 算 equity_rel 和 difference_B_E。 把 equity_rel 和 R^2 揉进去,能造出「相对利润*R^2」「相对利润*R^2/标准误差」这类组合。想让某个维度权重更高,就塞一个 ratio 变量。最终凑出 12 个自定义准则,从纯 R^2 到 profit_equity_r2_R 都覆盖。外汇和贵金属杠杆高,回测优的准则实盘也可能因点差滑点失效,验证时务必用真实tick。 下面这段是 equityDD 与组合系数的核心片段,逐行看逻辑: Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT]; // 相对盈利=净利润/初始保证金 double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total) // 定义回撤计算函数,传入余额、净值、统计数组、系数数组和总长度 { if(TeSt[STAT_INITIAL_DEPOSIT] == 0) return(0); // 初始保证金为0直接返回0避免除零 double Balance_max = Balance[0]; // 初始化历史最高余额 double Equity_min = Equity[0]; // 初始化最低净值 difference_B_E = 0; // 余额减净值的最大落差清零 double Max_Balance = 0; // 临时存最高余额 switch((int)TeSt[41]) // 根据采集方式分支 { case 0: difference_B_E = TeSt[STAT_EQUITY_DD]; // 方式0直接用MT5统计的净值回撤 break; default: for(int i = 0; i < total - 1; i++) // 遍历每根历史 { if(Balance_max < Balance[i]) // 更新历史最高余额 Balance_max = Balance[i]; if(Balance[i] == 10963) Sleep(1); // 调试用断点,可删 if(Balance_max - Equity[i + 1] > difference_B_E) // 若当前落差大于记录值 { Equity_min = Equity[i + 1]; // 刷新最低净值 difference_B_E = Balance_max - Equity_min; // 刷新最大落差 Max_Balance = Balance_max; // 记录当时最高余额 } } break; } return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]); // 返回 equity_rel:1减回撤占比 } coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E; // 挽回因子=利润/最大落差 coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2]; // 挽回因子乘R^2 coeff[profit_r2] = relative_prof * coeff[r2]; // 相对利润乘R^2 if(stand_err == 0) coeff[profit_r2_Err] = 0; // 标准误差为0则商为0 else coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err; // 否则算入标准误差 relative_prof + equity_rel + r2; // 占位示意三量相加 coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2]; // 利润加权 coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2]; // 回撤加权 coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio; // 直线度加权

MQL5 / C++
Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];
class="type">class="kw">double equityDD(class="kw">const class="type">class="kw">double & Balance[], class="kw">const class="type">class="kw">double & Equity[], class="kw">const class="type">class="kw">double & TeSt[], class="kw">const class="type">class="kw">double & coeff[], class="kw">const class="type">int total)
  {
   if(TeSt[STAT_INITIAL_DEPOSIT] == class="num">0)
     class="kw">return(class="num">0);
   class="type">class="kw">double Balance_max = Balance[class="num">0];
   class="type">class="kw">double Equity_min = Equity[class="num">0];
   difference_B_E = class="num">0;
   class="type">class="kw">double Max_Balance = class="num">0;
   class="kw">switch((class="type">int)TeSt[class="num">41])
     {
      case  class="num">0:
       difference_B_E = TeSt[STAT_EQUITY_DD];
       class="kw">break;
      class="kw">default:
       for(class="type">int i = class="num">0; i < total - class="num">1; i++)
         {
          if(Balance_max < Balance[i])
           Balance_max = Balance[i];
          if(Balance[i] == class="num">10963)
           Sleep(class="num">1);
          if(Balance_max - Equity[i + class="num">1] > difference_B_E)
            {
             Equity_min = Equity[i + class="num">1];
             difference_B_E = Balance_max - Equity_min;
             Max_Balance = Balance_max;
            }
         }
       class="kw">break;
     }
   class="kw">return(class="num">1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]);
  }
coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;
coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];
coeff[profit_r2] = relative_prof * coeff[r2];
  if(stand_err == class="num">0)
     coeff[profit_r2_Err] = class="num">0;
  else
     coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;
relative_prof + equity_rel + r2;
coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2];
coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2];
coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;

用 profit_equity_R_r2 挑出更平滑的曲线

把两个简单 EA 直接跑出来,结果都没达到预期,于是换用一个会实际下单的 EA 来展示优化效果,EA 名字先隐去。假设时间到了四月底,准备上真仓,核心问题是:用什么优化准则,才更可能在接下来实盘里获利。 启动一次三个月窗口的前向优化,跑完重启终端,用脚本只勾选最优结果。我从所有结果里按视觉挑了前向通测最好的一条,几个候选差不多,最终选了 profit_equity_R_r2,因为这组优化把低回撤放在优先位。 和最大余额、最大复杂度两组比对,profit_equity_R_r2 下的成交笔数明显更少,利润量级接近,但净值曲线平滑得多。这就把自定义准则钉在了 profit_equity_R_r2 上。 回测验证:若过去三个月按 profit_equity_R_r2 优化,再把所得参数用于 4 月 1 日至 5 月 1 日交易,账面约 +750 单位,净值回撤约 300 单位。外汇与贵金属杠杆高,这类前向结果只代表历史窗口概率,不等于未来收益。 再用 fxsaber 的 Validate EA 做四个月的「优化三月+交易一月」压力测试,EA 扛住了;同参数但基于最大复杂度优化的对照图,EA 虽没爆,但曲线难看得多。

「没有万能准则,但准则库已经成型」

实验跑下来一个很实在的结论:不存在哪一种自定义优化准则能通吃所有 EA。同一套数据里,有的系统用最大回撤加权跑出更稳的参数,有的反而靠盈利因子优先才拿到最优解,所谓「独占矛头的超级准则」并不存在。 但我们手里的这一组准则已经覆盖了多数常见逻辑——均值回归、突破跟随、网格加仓各自都有适配项。如果你认可用多准则并行筛选的思路,实际可挑的组合比策略测试器默认那几项宽得多。 外汇与贵金属市场高杠杆、滑点跳空频发,任何优化结果都只是历史样本下的概率倾向,不能直接当成实盘保底。 有测试者提过一嘴:与其让用户啃文档,不如直接把脚本配置摊开。对不想钻细节的人,复制代码改几个输入变量就能在 MT5 里跑,比读原理更快落地。

◍ 把快照优化器挂进自己的EA

这套快照优化逻辑以头文件形式分发,先取附带的 zip 解压,丢进 MQL5 数据目录。终端里走「文件→打开数据文件夹」,在空白处右键选插入,若弹窗问是否覆盖,直接点替换即可。 随后开 MetaEditor 载入你的 EA 做三处改动:OnTick() 内调 IsOnTick();若已有 OnTradeTransaction() 就在里面接 IsOnTradeTransaction(trans, request, result);底部补一段包含 OnTester 返回值的代码。改完按编译。 编译报错多是变量名撞车,按提示改个名就能过。外汇与贵金属品种波动剧烈、杠杆风险高,挂上后建议先在策略测试器用历史数据跑一遍确认无异常再上实盘。

MQL5 / C++
  class="macro">#include <SkrShotOpt.mqh>    
  class="type">class="kw">double OnTester() {class="kw">return(IsOnTester());}
  class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction & trans, class="kw">const class="type">MqlTradeRequest & request,class="kw">const class="type">MqlTradeResult & result) 
    {
      IsOnTradeTransaction(trans, request, result);
     }

EA 面板里那几行隐藏开关怎么调

把代码塞进 EA 之后,设置面板会多出几行参数。先说死一条:这些新增项绝对不要勾进优化框,它们不参与遗传优化计算,勾了只会干扰批次还拖慢速度。 写盘门槛有两个实用旋钮。「Write the pass if trades more than」管交易笔数,你的 EA 在单遍回测里狂刷几百单时,调大这个值能砍掉帧数据文件的体量;「Write the pass if profit exceeds %」默认会删亏损通测,想保留高于初始余额某百分比的结果就改它,比如设 5 即只留利润超 5% 的遍次。 「Select equity values」直接决定一批自定义准则能不能算准。要算 equity_rel、c_recovery_factor、profit_r2_Err 这类带权益依赖的项,选 save only min equity;想要和策略测试器里一模一样的权益带图形,选 save min and max equity。 「Custom criterion」设 none 时帧照写不误,但完全不跑准则计算、也不许你按准则筛优;真要按准则挑,就得在这里指名某一个,且别忘了它依赖上一项的权益选取和「Calculate criterion by」的基准切换(余额或净值影响所有含 R^2 的准则)。 脚本侧,「Draw regression line」挑画哪条回归线;「Profit percent more than」卡掉低利润截图省时间;「Only best results」开 true 只存每准则最优帧。Graph 项切 Back separately from Forward(前向从初始余额起)或 Back continued by Forward(前向接回测末尾余额),两种净值轨迹差异在加仓策略里能差出整段曲线形态。

把多准则巡检交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多准则优化后的净值形态与排序,你只管判读哪条曲线更抗回撤。

常见问题

每次即时报价写盘会让帧文件膨胀、处理变慢,改在 OnTradeTransaction 跟踪持仓变化,仅在开平仓节点落数组更轻量。
默认只存可盈利通测,设置里可另开亏损登记;前向通测也都会记录,方便后续做样本外核对。
可以,小布盯盘内置的 AIGC 读入 FrameAdd 输出的帧数据后能重绘净值并排多准则,不用自己拼脚本。
该情况只写一笔持仓,图形可视化不受影响,且能大幅压缩数组规模,属于刻意做的降噪处理。
按升序对图形排完序后取头部即最优结果,具体阈值在 CustomCriterion 文件里改,不必动 EA 主代码。