为智能系统制定品质因数·进阶篇
◍ 多空持仓的追踪止损改写逻辑
这段 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 才改。
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>() { <span class="comment">//--- Average profit</span> <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> <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> <span class="keyword">double</span> rr_medio = lucro_medio / prejuizo_medio; <span class="comment">//---</span> <span class="keyword">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, 2); } <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() { <span class="comment">//--- Average profit</span> <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> <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> <span class="keyword">double</span> rr_medio = lucro_medio / prejuizo_medio; <span class="comment">//---</span> <span class="keyword">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, 2); } <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | OnTester | </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() { <span class="keyword">return</span> rr_medio(); } [/CODE] 逐行拆解:第 1 行定义 OnTester 返回双精度;lucro_medio 取总盈利除以(盈利笔数+1);prejuizo_medio 取总亏损负值除以(亏损笔数+1);rr_medio 为两者商,即风险回报比;NormalizeDouble(...,2) 截断到两位小数。下方 rr_medio() 是同样逻辑抽出的函数,宏定义让 EA 本体和 include 文件不会撞名。 外汇与贵金属交易自带高杠杆风险,回测里的 1.23 只是历史样本下的倾向,实盘可能明显偏离。
<span class="keyword">class="type">class="kw">double</span> <span class="functions">OnTester</span>() { <span class="comment">class=class="str">"cmt">//--- Average profit</span> <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> <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> <span class="keyword">class="type">class="kw">double</span> rr_medio = lucro_medio / prejuizo_medio; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, class="num">2); } <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() { <span class="comment">class=class="str">"cmt">//--- Average profit</span> <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> <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> <span class="keyword">class="type">class="kw">double</span> rr_medio = lucro_medio / prejuizo_medio; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">return</span> <span class="functions">NormalizeDouble</span>(rr_medio, class="num">2); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| OnTester |</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() { <span class="keyword">class="kw">return</span> rr_medio(); }
「改版 CPC 指数的实现思路」
早期品质评分沿用 Sunny Harris 的 CPC 指数框架,把风险回报比、胜率、盈利率三者相乘。原版用盈利因子单维度衡量回撤恢复能力,这里改成取盈利因子与恢复因子的最小值,实测比单用盈利因子更能暴露策略在连亏后的修复薄弱点。 调用时只需在 OnTester 里跑一次 CPCIndex,注意函数内没有给交易总次数加 1——传入的统计值已假定至少有 1 笔成交可供评估,空跑会除零。 外汇与贵金属品种杠杆高、滑点跳空频繁,该指数仅反映历史测试样本下的综合品质,实盘表现可能偏离,请先在 MT5 策略测试器用自有品种验证。
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 是否藏着尖峰。
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;