为智能系统制定品质因数(基础篇)
给智能交易系统定一个品质因数
做 EA 之前先想清楚用什么指标筛掉垃圾策略。MQL5 官方示例里把「品质因数(Figure of Merit)」单独拎出来,本质是用一个标量把回测表现压缩成可排序的数字,避免人肉翻几十张 equity 曲线。 下面这段是原文给出的核心计算片段,直接决定系统能不能进实盘观察池。它把总收益、最大回撤和交易次数揉成一个值,数值越高代表单位风险下的性价比越能看。 外汇与贵金属品种波动跳空频繁,这套因数只在历史样本内有效,换周期或换品种后倾向需要重算,实盘前务必在 MT5 策略测试器里跑一遍验证。
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——因为还没写品质因数计算函数。外汇与贵金属属高风险品种,这类随机系统实盘亏损概率极高,仅作框架验证用。
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 修改单。 外汇与贵金属杠杆高,这类纯随机入场仅适合做机制验证,实盘直接跑大概率磨损手续费与点差。
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));