为智能系统制定品质因数·进阶篇
📘

为智能系统制定品质因数·进阶篇

第 2/3 篇

◍ 多空持仓的追踪止损改写逻辑

这段 MT5 EA 片段处理的是已有持仓下的止损移动:只针对 BUY 或 SELL 两种方向,用最近两根 K 线的高低点重算止损,再判断是否比原 SL 更优。 对多头而言,条件是前一根高点高于当前根高点(cotacoes[1].high > cotacoes[0].high),此时止损取两根 K 线低点的最小值再减 offset[0],相当于把止损压到更紧的下方支撑外。若算出的 sl 大于原 SL,才调用 PositionModify 改写,避免往亏损方向拉宽止损。 空头分支镜像处理:前一根低点低于当前根低点时才动,止损取两根高点最大值加 offset[0];与原 SL 的比较写成「SL==0 或 sl 介于 0 和原 SL 之间」才修改,逻辑上只允许止损向盈利方向移动。外汇与贵金属杠杆高,这类自动改止损若 offset 设太小,遇毛刺可能被扫后反向,建议先在策略测试器用 2023 年 XAUUSD 的 M5 数据跑一遍验证触发频率。 逐行拆解如下:if(tipo==POSITION_TYPE_BUY) 判断当前持仓为多单;内层 if(cotacoes[1].high>cotacoes[0].high) 确认价格没创新高;double sl=MathMin(cotacoes[0].low,cotacoes[1].low)-offset[0] 计算新止损价;info.NormalizePrice(sl) 按品种精度规范化;if(sl>SL) 保证只上移止损;negocios.PositionModify(_Symbol,sl,TP) 执行修改。else 分支对空单用 MathMax 加 offset,且条件放宽为 SL==0 或 sl<SL 才改。

MQL5 / C++
if(tipo == POSITION_TYPE_BUY)
  {
    if (cotacoes[class="num">1].high > cotacoes[class="num">0].high)
      {
        class="type">class="kw">double sl = MathMin(cotacoes[class="num">0].low, cotacoes[class="num">1].low) - offset[class="num">0];
        info.NormalizePrice(sl);
        if (sl > SL)
          {
            negocios.PositionModify(_Symbol, sl, TP);
          }
      }
  }
else class=class="str">"cmt">// tipo == POSITION_TYPE_SELL
  {
    if (cotacoes[class="num">1].low < cotacoes[class="num">0].low)
      {
        class="type">class="kw">double sl = MathMax(cotacoes[class="num">0].high, cotacoes[class="num">1].high) + offset[class="num">0];
        info.NormalizePrice(sl);
        if (SL == class="num">0 || (sl > class="num">0 && sl < SL))
          {
            negocios.PositionModify(_Symbol, sl, TP);
          }
      }
  }
}
class="kw">return true;
  }
  class=class="str">"cmt">// there was no position
  class="kw">return class="kw">false;
}

把平均风险回报率塞进回测报告

想在 MT5 的策略测试器报告里直接看到「OnTester result」,只需自己写一个返回 double 的 OnTester 函数。EA 跑完回测,这个值就会出现在报告里,不用额外导出数据再算。 我们算的是平均风险回报率:假设风险恒定为 1,回报就是每冒 1 单位险拿回多少。比如 USDJPY 在 2023-01-01 至 2023-05-19、H1、OHLC 模式下跑出来的结果里,比率可能呈现 1.23(赚)或 0.43(亏),贴近 1.00 就是盈亏平衡。 统计里没有现成的平均盈亏额,所以用总盈利除以盈利交易数(+1 防零),总亏损同理取负。加 1 是为了没交易时也不除零崩溃。最后用 NormalizeDouble 只留两位小数,免得报告刷出五六位小数看着晕。 下面这段代码直接接在你 EA 末尾就能用。注意我把函数抽去了 ARTICLE_METRICS.mq5,用 #include 引入;用宏 SQN_TESTER_ON_TESTER 包一层,EA 里自己写了 OnTester 就注释掉 include 那行,避免重定义。 [CODE] <span class="keyword">double</span> <span class="functions">OnTester</span>() &nbsp;&nbsp;{ <span class="comment">//--- Average profit</span> &nbsp;&nbsp; <span class="keyword">double</span> lucro_medio=<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_PROFIT</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_PROFIT_TRADES</span>)+<span class="number">1</span>); <span class="comment">//--- Average loss</span> &nbsp;&nbsp; <span class="keyword">double</span> prejuizo_medio=-<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_LOSS</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_LOSS_TRADES</span>)+<span class="number">1</span>); <span class="comment">//--- Risk calculation: profitability to be returned</span> &nbsp;&nbsp; <span class="keyword">double</span> rr_medio = lucro_medio / prejuizo_medio; <span class="comment">//---</span> &nbsp;&nbsp; <span class="keyword">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, 2); &nbsp;&nbsp;} <span class="preprocessor">#include </span><span class="string">"ARTICLE_METRICS.mq5"</span> <span class="comment">//--- Risk calculation: average return on operation</span> <span class="keyword">double</span> rr_medio() &nbsp;&nbsp;{ <span class="comment">//--- Average profit</span> &nbsp;&nbsp; <span class="keyword">double</span> lucro_medio=<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_PROFIT</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_PROFIT_TRADES</span>)+<span class="number">1</span>); <span class="comment">//--- Average loss</span> &nbsp;&nbsp; <span class="keyword">double</span> prejuizo_medio=-<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_LOSS</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_LOSS_TRADES</span>)+<span class="number">1</span>); <span class="comment">//--- Risk calculation: profitability to be returned</span> &nbsp;&nbsp; <span class="keyword">double</span> rr_medio = lucro_medio / prejuizo_medio; <span class="comment">//---</span> &nbsp;&nbsp; <span class="keyword">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, 2); &nbsp;&nbsp;} <span class="comment">//+------------------------------------------------------------------+</span>

<span class="comment">//OnTester&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;&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">//+------------------------------------------------------------------+</span> <span class="preprocessor">#ifndef </span>SQN_TESTER_ON_TESTER <span class="preprocessor">#define </span>SQN_TESTER_ON_TESTER <span class="functions">OnTester</span> <span class="preprocessor">#endif </span><span class="keyword">double</span> SQN_TESTER_ON_TESTER() &nbsp;&nbsp;{ &nbsp;&nbsp; <span class="keyword">return</span> rr_medio(); &nbsp;&nbsp;} [/CODE] 逐行拆解:第 1 行定义 OnTester 返回双精度;lucro_medio 取总盈利除以(盈利笔数+1);prejuizo_medio 取总亏损负值除以(亏损笔数+1);rr_medio 为两者商,即风险回报比;NormalizeDouble(...,2) 截断到两位小数。下方 rr_medio() 是同样逻辑抽出的函数,宏定义让 EA 本体和 include 文件不会撞名。 外汇与贵金属交易自带高杠杆风险,回测里的 1.23 只是历史样本下的倾向,实盘可能明显偏离。

MQL5 / C++
<span class="keyword">class="type">class="kw">double</span> <span class="functions">OnTester</span>()
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- Average profit</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> lucro_medio=<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_PROFIT</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_PROFIT_TRADES</span>)+<span class="number">class="num">1</span>);
<span class="comment">class=class="str">"cmt">//--- Average loss</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> prejuizo_medio=-<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_LOSS</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_LOSS_TRADES</span>)+<span class="number">class="num">1</span>); 
<span class="comment">class=class="str">"cmt">//--- Risk calculation: profitability to be returned</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> rr_medio = lucro_medio / prejuizo_medio;
<span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, class="num">2);
&nbsp;&nbsp;}
<span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"ARTICLE_METRICS.mq5"</span>
<span class="comment">class=class="str">"cmt">//--- Risk calculation: average class="kw">return on operation</span>
<span class="keyword">class="type">class="kw">double</span> rr_medio()
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- Average profit</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> lucro_medio=<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_PROFIT</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_PROFIT_TRADES</span>)+<span class="number">class="num">1</span>);
<span class="comment">class=class="str">"cmt">//--- Average loss</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> prejuizo_medio=-<span class="functions">TesterStatistics</span>(<span class="macro">STAT_GROSS_LOSS</span>)/(<span class="functions">TesterStatistics</span>(<span class="macro">STAT_LOSS_TRADES</span>)+<span class="number">class="num">1</span>); 
<span class="comment">class=class="str">"cmt">//--- Risk calculation: profitability to be returned</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> rr_medio = lucro_medio / prejuizo_medio;
<span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, class="num">2);
&nbsp;&nbsp;}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| OnTester&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;&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="preprocessor">class="macro">#ifndef </span>SQN_TESTER_ON_TESTER
<span class="preprocessor">class="macro">#define </span>SQN_TESTER_ON_TESTER <span class="functions">OnTester</span>
<span class="preprocessor">class="macro">#endif
</span><span class="keyword">class="type">class="kw">double</span> SQN_TESTER_ON_TESTER()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="kw">return</span> rr_medio();
&nbsp;&nbsp;}

「改版 CPC 指数的实现思路」

早期品质评分沿用 Sunny Harris 的 CPC 指数框架,把风险回报比、胜率、盈利率三者相乘。原版用盈利因子单维度衡量回撤恢复能力,这里改成取盈利因子与恢复因子的最小值,实测比单用盈利因子更能暴露策略在连亏后的修复薄弱点。 调用时只需在 OnTester 里跑一次 CPCIndex,注意函数内没有给交易总次数加 1——传入的统计值已假定至少有 1 笔成交可供评估,空跑会除零。 外汇与贵金属品种杠杆高、滑点跳空频繁,该指数仅反映历史测试样本下的综合品质,实盘表现可能偏离,请先在 MT5 策略测试器用自有品种验证。

MQL5 / C++
class=class="str">"cmt">//--- Calculating CPC Index by Sunny Harris
class="type">class="kw">double CPCIndex()
  {
   class="type">class="kw">double taxa_acerto=TesterStatistics(STAT_PROFIT_TRADES)/TesterStatistics(STAT_TRADES);
   class="type">class="kw">double fator=MathMin(TesterStatistics(STAT_PROFIT_FACTOR), TesterStatistics(STAT_RECOVERY_FACTOR));
   class="kw">return NormalizeDouble(fator * taxa_acerto * rr_medio(), class="num">5);
  }

◍ 用 SQN 揪出尖峰型系统

Van Tharp 提出的系统品质指数(SQN)看重的是系统稳定性,而非单纯月末盈亏。它会对明显尖峰惩罚:若一堆小亏加一笔大赚,或几笔小赚配一笔大亏,那笔异常交易会被压低评分,后者对交易者来说是最糟的形态。 计算上,先由 dp_por_negocio 用全部成交算收益标准差;最终 sqn 函数把交易数截断到 100 笔再开方,避免单月交易过多虚抬数值。sqn_mes 负责识别新月份并累积数据,模拟结束给出月度平均 SQN。 外汇与贵金属属高风险品类,月度 SQN 波动可能很大,别只盯月底余额。开 MT5 跑一遍下面代码,在 OnTester 里打印三个值,就能直接比对你的 EA 是否藏着尖峰。

MQL5 / C++
class=class="str">"cmt">//--- standard deviation of executed trades based on results in money
class="type">class="kw">double dp_por_negocio(class="type">uint primeiro_negocio, class="type">uint ultimo_negocio,
                      class="type">class="kw">double media_dos_resultados, class="type">class="kw">double quantidade_negocios)
  {
   class="type">class="kw">ulong ticket=class="num">0;
   class="type">class="kw">double dp=class="num">0.0;
   for(class="type">uint i=primeiro_negocio; i < ultimo_negocio; i++)
     {
      class=class="str">"cmt">//--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>class="num">0)
        {
         class=class="str">"cmt">//--- get deals properties
         class="type">class="kw">double profit=HistoryDealGetDouble(ticket,DEAL_PROFIT);
         class=class="str">"cmt">//--- create price object
         if(profit!=class="num">0)
           {
            dp += MathPow(profit - media_dos_resultados, class="num">2.0);
           }
        }
     }
   class="kw">return MathSqrt(dp / quantidade_negocios);
  }
class=class="str">"cmt">//--- Calculation of System Quality Number, SQN, by Van Tharp
class="type">class="kw">double sqn(class="type">uint primeiro_negocio, class="type">uint ultimo_negocio,
           class="type">class="kw">double lucro_acumulado, class="type">class="kw">double quantidade_negocios)
  {
   class="type">class="kw">double lucro_medio = lucro_acumulado / quantidade_negocios;
   class="type">class="kw">double dp = dp_por_negocio(primeiro_negocio, ultimo_negocio,
                              lucro_medio, quantidade_negocios);
   if(dp == class="num">0.0)
     {
      class=class="str">"cmt">// Because the standard deviation returned a value of zero, which we didn&class="macro">#x27;t expect
      class=class="str">"cmt">// we change it to average_benefit, since there is no deviation, which
      class=class="str">"cmt">// brings the system closer to result class="num">1.
      dp = lucro_medio;
     }
class=class="str">"cmt">//--- The number of trades here will be limited to class="num">100, so that the result will not be
class=class="str">"cmt">//--- maximized due to the large number of trades.
   class="type">class="kw">double res = (lucro_medio / dp) * MathSqrt(MathMin(class="num">100, quantidade_negocios));
   class="kw">return NormalizeDouble(res, class="num">2);
  }
class=class="str">"cmt">//--- returns if a new month is found
class="type">bool eh_um_novo_mes(class="type">class="kw">datetime timestamp, class="type">int &mes_anterior)
  {
   class="type">MqlDateTime mdt;
   TimeToStruct(timestamp, mdt);
   if(mes_anterior < class="num">0)
     {
      mes_anterior=mdt.mon;
     }
   if(mes_anterior != mdt.mon)
     {
      mes_anterior = mdt.mon;
      class="kw">return true;
     }
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Monthly SQN
class="type">class="kw">double sqn_mes(class="type">void)
  {
   class="type">class="kw">double sqn_acumulado = class="num">0.0;

常见问题

在改写逻辑里用持仓方向变量分流:多头用近期低点减偏移,空头用近期高点加偏移,各自维护止损线,不共用同一参数。
在每笔交易出场时记录盈亏与事前风险(止损距离×仓位),循环结束后用总盈利/总风险得出平均风险回报率,直接写进报告尾部。
可以。把策略的逐笔收益喂给小布,它会用SQN类指标算出分布形态,尖峰过高且样本少时直接提示脆弱,省去你自己跑脚本。
在原CPC的分母处乘一个ATR归一系数,分子保留单位时间净利,再把阈值随品种波动率动态缩放即可。
SQN低于1.6倾向尖峰型脆弱系统;多数稳健波段系统落在2.0~4.0,超过5多为高频率但需防过拟合。