为智能系统制定品质因数(基础篇)
📘

为智能系统制定品质因数(基础篇)

第 1/3 篇

给智能交易系统定一个品质因数

做 EA 之前先想清楚用什么指标筛掉垃圾策略。MQL5 官方示例里把「品质因数(Figure of Merit)」单独拎出来,本质是用一个标量把回测表现压缩成可排序的数字,避免人肉翻几十张 equity 曲线。 下面这段是原文给出的核心计算片段,直接决定系统能不能进实盘观察池。它把总收益、最大回撤和交易次数揉成一个值,数值越高代表单位风险下的性价比越能看。 外汇与贵金属品种波动跳空频繁,这套因数只在历史样本内有效,换周期或换品种后倾向需要重算,实盘前务必在 MT5 策略测试器里跑一遍验证。

MQL5 / C++
class="type">class="kw">double FigureOfMerit(class="type">class="kw">double profit, class="type">class="kw">double maxDrawdown, class="type">int trades)
  {
   if(maxDrawdown<=class="num">0 || trades<=class="num">0) class="kw">return(class="num">0);
   class="kw">return(profit / maxDrawdown * sqrt(trades));
  }

◍ 策略测试器里那个品质得分从哪来

在 MT5 策略测试器的回测报告里,有一个容易被忽略的字段:OnTester result。它本质上是你自己在智能系统中定义、并由测试器在优化或单次回测结束时返回的一个数值,用来给这套参数组合打一个“品质分”。 图例里实测出现过 1.0639375 这样一个返回值,说明当时那组参数跑出来的系统品质被记为略高于 1 的基准。这个值不是 MT5 内置的夏普或盈利因子,而是你用代码算出来再吐回去的,所以完全可控。 本文后续会给出两种测量系统品质的思路,并且要点在于:测试器一次只让你返回一个数值,所以如果你算了多个指标,得想清楚怎么压缩或取舍,只留一个写进 OnTester。

「用随机信号搭一个可测的裸EA骨架」

先不纠结系统品质因数,得有个能在策略测试器里跑起来的基本系统。这里选了极简做法:取一个随机数,偶数就最小手数做多,奇数就最小手数做空;若抽到 0 或 32767 则不动。随机数由 MathRand() 提供,范围是 0 到 32767。 持仓管理上加了两条互补规则。其一是沿每根新烛条方向移动止损:用 1 周期 ATR 经 8 周期 EMA 平滑后的值作为步长,把止损挂在两根分析烛条的最远端。其二是时间窗限制——11:00 到 16:00 之外不允许开仓,16:30 必须平仓。 代码里用 OnTimer 每秒触发,而非 OnTick,避免每笔成交都跑大函数。arruma_stop_em_posicoes 负责挂止损,返回 true 就说明已处理,主流程不必再走。OnTick 只置一个 tem_tick 标志,表示有新行情;若休市期间也强行调函数,测试器会无故暂停。 两个窗口分开定义:开仓窗 11:00–16:00,管理窗延续到 16:30 强平。用该 EA 在 USDJPY 上测 2023-01-01 至 2023-05-19、H1、OHLC 模式,OnTester result 会是 0——因为还没写品质因数计算函数。外汇与贵金属属高风险品种,这类随机系统实盘亏损概率极高,仅作框架验证用。

MQL5 / C++
class=class="str">"cmt">//--- Indicator ATR(class="num">1) with EMA(class="num">8) used for the stop level...
class="type">int ind_atr = iATR(_Symbol, PERIOD_CURRENT, class="num">1);
class="type">int ind_ema = iMA(_Symbol, PERIOD_CURRENT, class="num">8, class="num">0, MODE_EMA, ind_atr);
class=class="str">"cmt">//--- Define a variable that indicates that we have a deal...
class="type">bool tem_tick = class="kw">false;
class=class="str">"cmt">//--- An auxiliary variable for opening a position
class="macro">#include<Trade/Trade.mqh>
class="macro">#include<Trade/SymbolInfo.mqh>
CTrade negocios;
CSymbolInfo info;
class=class="str">"cmt">//--- Define in OnInit() the use of the timer every second
class=class="str">"cmt">//--- and start CTrade
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Set the fill type to keep a pending order
class=class="str">"cmt">//--- until it is fully filled
   negocios.SetTypeFilling(ORDER_FILLING_RETURN);
class=class="str">"cmt">//--- Leave the fixed deviation at it is not used on B3 exchange
   negocios.SetDeviationInPoints(class="num">5);
class=class="str">"cmt">//--- Define the symbol in CSymbolInfo...
   info.Name(_Symbol);
class=class="str">"cmt">//--- Set the timer...
   EventSetTimer(class="num">1);
class=class="str">"cmt">//--- Set the base of the random number to have equal tests...
   MathSrand(0xDEAD);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//--- Since we set a timer, we need to destroy it in OnDeInit().
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   EventKillTimer();
  }
class=class="str">"cmt">//--- The OnTick function only informs us that we have a new deal
class="type">void OnTick()
  {
   tem_tick = true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert Advisor main function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
  {
   class="type">MqlRates cotacao[];
   class="type">bool fechar_tudo = class="kw">false;
   class="type">bool negocios_autorizados = class="kw">false;
class=class="str">"cmt">//--- Do we have a new trade?
   if(tem_tick == class="kw">false)
      class="kw">return ;
class=class="str">"cmt">//--- To check, class="kw">return information of the last class="num">3 candlesticks....
   if(CopyRates(_Symbol, PERIOD_CURRENT, class="num">0, class="num">3, cotacao) != class="num">3)
      class="kw">return ;
class=class="str">"cmt">//--- Is there a new candlestick since the last check?
   if(tem_vela_nova(cotacao[class="num">2]) == class="kw">false)
      class="kw">return ;
class=class="str">"cmt">//--- Get data from the trade window and closing...
   negocios_autorizados = esta_na_janela_de_negocios(cotacao[class="num">2], fechar_tudo);
class=class="str">"cmt">//--- If we are going to close everything and if there is a position, close it...
   if(fechar_tudo)
     {
      negocios.PositionClose(_Symbol);

用随机数与时间窗拼出裸逻辑下单

这段尾部代码把开仓决策彻底交给了 MathRand() 的奇偶性。sorteio 取值域是 0~32767,当等于 0 或 32767 时直接 return 不操作,其余偶数买、奇数卖,等价于约 50% 概率双向触发,没有任何价格条件过滤。 时间窗由 esta_na_janela_de_negocios 控制:hour 在 11~15 点(含 11 不含 16)视为交易时段,close_positions 置 false;hour==16 且分钟 >=30 才允许平仓。也就是说,如果你在 MT5 把系统时间调成其他时段,机器人只会移动止损而不会开新仓。 arruma_stop_em_posicoes 用 CopyBuffer 取 EMA 缓冲偏移,再用 PositionSelect(_Symbol) 抓当前品种持仓。这里只做了 EMA 复制成功且选中持仓的判断,并未在片段内写出 SL 重设的具体赋值语句,实盘前需确认后续是否补了 NormalizePrice 与 SL 修改单。 外汇与贵金属杠杆高,这类纯随机入场仅适合做机制验证,实盘直接跑大概率磨损手续费与点差。

MQL5 / C++
  class="kw">return ;
    }
class=class="str">"cmt">//--- if we are not closing everything, move stop level if there is a position...
   if(arruma_stop_em_posicoes(cotacao))
      class="kw">return ;
   if (negocios_autorizados == class="kw">false) class=class="str">"cmt">// are we outside the trading window?
      class="kw">return ;
class=class="str">"cmt">//--- We are in the trading window, try to open a new position!
   class="type">int sorteio = MathRand();
class=class="str">"cmt">//--- Entry rule class="num">1.1
   if(sorteio == class="num">0 || sorteio == class="num">32767)
      class="kw">return ;
   if(MathMod(sorteio, class="num">2) == class="num">0)   class=class="str">"cmt">// Draw rule class="num">1.2 -- even number - Buy
    {
    negocios.Buy(info.LotsMin(), _Symbol);
    }
   else class=class="str">"cmt">// Draw rule class="num">1.3 -- odd number - Sell
    {
    negocios.Sell(info.LotsMin(), _Symbol);
    }
   }
class=class="str">"cmt">//--- Check if we have a new candlestick...
class="type">bool tem_vela_nova(class="kw">const class="type">MqlRates &rate)
  {
   class="kw">static class="type">class="kw">datetime vela_anterior = class="num">0;
   class="type">class="kw">datetime vela_atual = rate.time;
   if(vela_atual != vela_anterior) class=class="str">"cmt">// is time different from the saved one?
    {
      vela_anterior = vela_atual;
      class="kw">return true;
    }
   class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Check if the time is n the trade period to close positions...
class="type">bool esta_na_janela_de_negocios(class="kw">const class="type">MqlRates &rate, class="type">bool &close_positions)
  {
   class="type">MqlDateTime mdt;
   class="type">bool ret = class="kw">false;
   close_positions = true;
   if(TimeToStruct(rate.time, mdt))
    {
      if(mdt.hour >= class="num">11 && mdt.hour < class="num">16)
        {
         ret = true;
         close_positions = class="kw">false;
        }
      else
        {
         if(mdt.hour == class="num">16)
            close_positions = (mdt.min >= class="num">30);
        }
    }
   class="kw">return ret;
   }
class=class="str">"cmt">//---
class="type">bool arruma_stop_em_posicoes(class="kw">const class="type">MqlRates &cotacoes[])
  {
   if(PositionsTotal()) class=class="str">"cmt">// Is there a position?
    {
      class="type">class="kw">double offset[class="num">1] = { class="num">0 };
      if(CopyBuffer(ind_ema, class="num">0, class="num">1, class="num">1, offset) == class="num">1 class=class="str">"cmt">// EMA successfully copied?
       && PositionSelect(_Symbol))   class=class="str">"cmt">// Select the existing position!
       {
         class="type">ENUM_POSITION_TYPE tipo = (class="type">ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE);
         class="type">class="kw">double SL = PositionGetDouble(POSITION_SL);
         class="type">class="kw">double TP = info.NormalizePrice(PositionGetDouble(POSITION_TP));

常见问题

品质因数来自策略测试器对每笔交易的盈亏、持仓时长与回撤的综合评分,核心是把胜率与风险折算成一个可比数值,用来横向挑系统。
先用纯随机信号搭一个不下单逻辑的裸EA骨架,跑历史数据看得分分布,再逐步加时间窗过滤,对比得分变化即可。
小布可以读入你的EA回测摘要,自动标出随机区间得分与实盘逻辑得分的偏离,提醒你哪些品质因数只是拟合噪声。
不能。这类骨架只用于理解品质因数构造,实盘前必须替换成有价格行为依据的信号并过样本外检验。
概率较高。若微小参数变动导致得分剧烈跳变,倾向是过拟合;建议用walk-forward分段验证得分稳定性。