
为智能系统制定品质因数
概述
在本文中,我们将见识到如何制定一个品质得分,并由您的智能系统从策略测试器返回。 在下面的图例 1 中,您可以看到 “OnTester result” 值为 1.0639375,此即展示所执行系统的品质示例。 在本文中,我们将学到两种可能的方式来测量系统品质,并将看清如何记录这两个数值,因为我们只能返回其中一个值。
图例 1:高亮显示的 “OnTester result” 字段。
启动交易模型,并构建 EA
在解决系统品质因数之前,有必要创建一个能在测试中使用的基本系统。 我们选择了一个简单的系统:我们先选择一个随机数字,若它是偶数,我们就开仓做多;否则,我们开仓做空,因为数字是奇数。
为了保持随机抽取,我们调用 MathRand() 函数,它提供一个介于 0(零)至 32767 之间的数字。 此外,为了令系统更加平衡,我们将添加两条互补规则。 有了这三条规则,我们将努力确保系统更加可靠。 它们是:
- 当我们无持仓时,生成一个随机数
- 如果数字为 0 或 32767,则啥都不做
- 如果数字为偶数,则按最小手数的交易量做多资产
- 如果数字为奇数,则按最小手数的交易量做空资产
- 当我们有持仓时,沿每根新烛条的方向移动止损价位,该烛条在走势方向上高于前一根烛条
- 所用的止损价位将基于:1-周期的 ATR 指标值,经 8-周期 EMA 均化后的常规化值。 此外,它将置于距所分析两根烛条的最远端
- 如果时间在 11:00 到 16:00 的范围之外,我们不允许开仓,且在 16:30 必须平仓。
以下是制定这些规则的代码。
//--- Indicator ATR(1) with EMA(8) used for the stop level... int ind_atr = iATR(_Symbol, PERIOD_CURRENT, 1); int ind_ema = iMA(_Symbol, PERIOD_CURRENT, 8, 0, MODE_EMA, ind_atr); //--- Define a variable that indicates that we have a deal... bool tem_tick = false; //--- An auxiliary variable for opening a position #include<Trade/Trade.mqh> #include<Trade/SymbolInfo.mqh> CTrade negocios; CSymbolInfo info; //--- Define in OnInit() the use of the timer every second //--- and start CTrade int OnInit() { //--- Set the fill type to keep a pending order //--- until it is fully filled negocios.SetTypeFilling(ORDER_FILLING_RETURN); //--- Leave the fixed deviation at it is not used on B3 exchange negocios.SetDeviationInPoints(5); //--- Define the symbol in CSymbolInfo... info.Name(_Symbol); //--- Set the timer... EventSetTimer(1); //--- Set the base of the random number to have equal tests... MathSrand(0xDEAD); return(INIT_SUCCEEDED); } //--- Since we set a timer, we need to destroy it in OnDeInit(). void OnDeinit(const int reason) { EventKillTimer(); } //--- The OnTick function only informs us that we have a new deal void OnTick() { tem_tick = true; } //+------------------------------------------------------------------+ //| Expert Advisor main function | //+------------------------------------------------------------------+ void OnTimer() { MqlRates cotacao[]; bool fechar_tudo = false; bool negocios_autorizados = false; //--- Do we have a new trade? if(tem_tick == false) return ; //--- To check, return information of the last 3 candlesticks.... if(CopyRates(_Symbol, PERIOD_CURRENT, 0, 3, cotacao) != 3) return ; //--- Is there a new candlestick since the last check? if(tem_vela_nova(cotacao[2]) == false) return ; //--- Get data from the trade window and closing... negocios_autorizados = esta_na_janela_de_negocios(cotacao[2], fechar_tudo); //--- If we are going to close everything and if there is a position, close it... if(fechar_tudo) { negocios.PositionClose(_Symbol); return ; } //--- if we are not closing everything, move stop level if there is a position... if(arruma_stop_em_posicoes(cotacao)) return ; if (negocios_autorizados == false) // are we outside the trading window? return ; //--- We are in the trading window, try to open a new position! int sorteio = MathRand(); //--- Entry rule 1.1 if(sorteio == 0 || sorteio == 32767) return ; if(MathMod(sorteio, 2) == 0) // Draw rule 1.2 -- even number - Buy { negocios.Buy(info.LotsMin(), _Symbol); } else // Draw rule 1.3 -- odd number - Sell { negocios.Sell(info.LotsMin(), _Symbol); } } //--- Check if we have a new candlestick... bool tem_vela_nova(const MqlRates &rate) { static datetime vela_anterior = 0; datetime vela_atual = rate.time; if(vela_atual != vela_anterior) // is time different from the saved one? { vela_anterior = vela_atual; return true; } return false; } //--- Check if the time is n the trade period to close positions... bool esta_na_janela_de_negocios(const MqlRates &rate, bool &close_positions) { MqlDateTime mdt; bool ret = false; close_positions = true; if(TimeToStruct(rate.time, mdt)) { if(mdt.hour >= 11 && mdt.hour < 16) { ret = true; close_positions = false; } else { if(mdt.hour == 16) close_positions = (mdt.min >= 30); } } return ret; } //--- bool arruma_stop_em_posicoes(const MqlRates &cotacoes[]) { if(PositionsTotal()) // Is there a position? { double offset[1] = { 0 }; if(CopyBuffer(ind_ema, 0, 1, 1, offset) == 1 // EMA successfully copied? && PositionSelect(_Symbol)) // Select the existing position! { ENUM_POSITION_TYPE tipo = (ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE); double SL = PositionGetDouble(POSITION_SL); double TP = info.NormalizePrice(PositionGetDouble(POSITION_TP)); 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 // tipo == POSITION_TYPE_SELL { if (cotacoes[1].low < cotacoes[0].low) { double sl = MathMax(cotacoes[0].high, cotacoes[1].high) + offset[0]; info.NormalizePrice(sl); if (SL == 0 || (sl > 0 && sl < SL)) { negocios.PositionModify(_Symbol, sl, TP); } } } } return true; } // there was no position return false; }
我们简略研究一下上面的代码。 我们将经均化计算的 ATR 值来判定止损步长,即我们发现当前烛条超过前一根时,将止损价位放置在烛条的边界。 这是在函数 arruma_stop_em_posicoes 中完成的。 当其返回 true 时,已有设置,且我们无需在 OnTimer 的主代码中前进。 我使用该函数取代 OnTick,因为我不需要针对每笔执行的交易运行一遍大函数。 函数应该在所定义周期的每根新烛条建立时执行。 在 OnTick 中,设置 true 值表示前一笔交易。 这是必要的;否则,在休市期间,策略测试器将引入暂停,因为即使没有前一笔的交易,它也会执行该函数。
到该处为止,一切都严格按照定义好的计划进行,包括两个指定的窗口。 第一个是开仓交易的窗口,在 11:00 到 4:00 之间。 第二个是管理窗口,它允许算法管理持仓,移动其止损价位,直至 16:30 — 此时它应该把当天的所有交易了结。
请注意,如果我们现在用这个 EA 进行交易,“OnTester result” 将为零,如图例 2 所示,因为我们还没有为这个值提供计算函数。
图例 2: EA 针对 USDJPY,在 2023-01-01 至 2023-05-19 期间,OHLC 模式,H1 执行
关于品质因数
为了能够显示 “OnTester result” 值,我们需要定义返回双精度值的 OnTester 函数。 就这么简单! 在此,使用下面的代码,我们得到如图例 3 所示的结果。
图例 3: EA 针对 USDJPY,在 2023-01-01 至 2023-05-19 期间,OHLC 模式,H1 执行。
以下代码应放在上一段代码的末尾。 在此,我们计算交易的平均风险回报率:该比率通常表示为获取的回报,因为假设风险是恒定的,等于 1。 故此,我们可以将风险回报率解释为 1:1.23 或简单的 1.23,另一个例子可能是 0.43。 在第一个例子中,我们所冒风险,每投入 1 美元,就会收获 1.23 美元;而在第二个例子中,我们所冒风险,每投入 1 美元,只能得到 0.43 美元,亏损了。 因此,当回报为 1 或接近它时,这意味着我们收支平衡,高于它意味着我们赚了。
由于统计数据并未提供平均收益或亏损额度,我们将取总数值除以两侧(做多或做空)交易数量的常规值。 当返回已执行交易的数量时,要加 1。 这样的话,如果没有盈利交易或亏损交易,程序也不会在计算过程中因除零而终止。 此外,为了避免显示太多的小数位,如前面的图例 1 所示,其中小数位超过 5 位,我们调用 NormalizeDouble 仅显示结果的两位小数。
double OnTester() { //--- Average profit double lucro_medio=TesterStatistics(STAT_GROSS_PROFIT)/(TesterStatistics(STAT_PROFIT_TRADES)+1); //--- Average loss double prejuizo_medio=-TesterStatistics(STAT_GROSS_LOSS)/(TesterStatistics(STAT_LOSS_TRADES)+1); //--- Risk calculation: profitability to be returned double rr_medio = lucro_medio / prejuizo_medio; //--- return NormalizeDouble(rr_medio, 2); }
OnTester 函数必须存在于每个智能系统当中,以遍确保在报告中显示该数值。 为了尽量减少复制若干行代码的相关工作,我们将函数转移到一个单独的文件之中。 以这种方式,我们每次就只需要复制一行。 具体操作如下:
#include "ARTICLE_METRICS.mq5"
以这种方式,我们得到一段简洁的代码! 在指定的文件中,函数都已定义。 如果我们打算用 include 的话,这令我们能够轻松更改欲包含的函数名称,从而避免可能出现的 OnTester 函数重复定义错误。 如此,我们可以将当作一种机制,即优先使用直接插入到 EA 代码中的 OnTester。 如果我们打算由 include 来用它,我们简单地注释掉 EA 代码中的 OnTester 函数,并注释掉相应的宏定义即可。 我们稍后会回到这处。
起初,ARTICLE_METRICS.mq5 文件将如下所示:
//--- Risk calculation: average return on operation double rr_medio() { //--- Average profit double lucro_medio=TesterStatistics(STAT_GROSS_PROFIT)/(TesterStatistics(STAT_PROFIT_TRADES)+1); //--- Average loss double prejuizo_medio=-TesterStatistics(STAT_GROSS_LOSS)/(TesterStatistics(STAT_LOSS_TRADES)+1); //--- Risk calculation: profitability to be returned double rr_medio = lucro_medio / prejuizo_medio; //--- return NormalizeDouble(rr_medio, 2); } //+------------------------------------------------------------------+ //| OnTester | //+------------------------------------------------------------------+ #ifndef SQN_TESTER_ON_TESTER #define SQN_TESTER_ON_TESTER OnTester #endif double SQN_TESTER_ON_TESTER() { return rr_medio(); }
请注意,正确的文件名必须具有扩展名 “mqh”。 不过,由于我计划将文件保存于 Experts 目录,故我特意保留了代码扩展名。 您这边可自行决定。
第一版品质计算
我们第一个版本的品质计算是基于 Sunny Harris 创建的方法,名为 CPC 指数。 这种方式用到三个衡量度,它们互乘:风险:平均回报、成功率、和盈利率。 不过,我们要对其进行修改,令其不使用盈利因子 — 代之,取盈利因子和恢复因子两者间的最低值。 虽然,如果我们考虑两者之间的区别,我们应该选择恢复因子,但我更愿保持原样,因为它已经导致了计算的改进。
下面的代码实现了上一段中提到的方式。 我们只需要在 OnTester 中调用它。 请注意,我们于此没有把交易数量加 1,因为提供的数值是通用的,我们预计至少会有 1 笔交易可评估。
//--- Calculating CPC Index by Sunny Harris double CPCIndex() { double taxa_acerto=TesterStatistics(STAT_PROFIT_TRADES)/TesterStatistics(STAT_TRADES); double fator=MathMin(TesterStatistics(STAT_PROFIT_FACTOR), TesterStatistics(STAT_RECOVERY_FACTOR)); return NormalizeDouble(fator * taxa_acerto * rr_medio(), 5); }
第二版品质计算
我们将要探讨的第二版品质因数称为系统品质指数(SQN)。 它是由 Van Tharp 创造的。 我们将计算每个月执行的交易,并获取模拟的所有月份的简单平均值。 SQN 与上一章节中讲述的方式不同,因为它着重寻求的是交易系统的稳定性。
SQN 的一个重要特征就是它惩罚具有明显尖峰的系统。 因此,如果系统有一连串小笔交易和一笔大交易,则后者将受到惩罚。 这意味着,如果我们的系统有多笔小亏损和一笔大盈利,那么这笔盈利就会受到惩罚。 与其对比(小利大亏)也会受到惩罚。 后者对交易者来说是最糟糕的。
请记住,交易是一场长期的竞赛,请始终留意追踪您的系统! 不要只盯住您在月底赚到的钱,因为它可能会有极大的波动。
//--- standard deviation of executed trades based on results in money double dp_por_negocio(uint primeiro_negocio, uint ultimo_negocio, double media_dos_resultados, double quantidade_negocios) { ulong ticket=0; double dp=0.0; for(uint i=primeiro_negocio; i < ultimo_negocio; i++) { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) { //--- get deals properties double profit=HistoryDealGetDouble(ticket,DEAL_PROFIT); //--- create price object if(profit!=0) { dp += MathPow(profit - media_dos_resultados, 2.0); } } } return MathSqrt(dp / quantidade_negocios); } //--- Calculation of System Quality Number, SQN, by Van Tharp double sqn(uint primeiro_negocio, uint ultimo_negocio, double lucro_acumulado, double quantidade_negocios) { double lucro_medio = lucro_acumulado / quantidade_negocios; double dp = dp_por_negocio(primeiro_negocio, ultimo_negocio, lucro_medio, quantidade_negocios); if(dp == 0.0) { // Because the standard deviation returned a value of zero, which we didn't expect // we change it to average_benefit, since there is no deviation, which // brings the system closer to result 1. dp = lucro_medio; } //--- The number of trades here will be limited to 100, so that the result will not be //--- maximized due to the large number of trades. double res = (lucro_medio / dp) * MathSqrt(MathMin(100, quantidade_negocios)); return NormalizeDouble(res, 2); } //--- returns if a new month is found bool eh_um_novo_mes(datetime timestamp, int &mes_anterior) { MqlDateTime mdt; TimeToStruct(timestamp, mdt); if(mes_anterior < 0) { mes_anterior=mdt.mon; } if(mes_anterior != mdt.mon) { mes_anterior = mdt.mon; return true; } return false; } //--- Monthly SQN double sqn_mes(void) { double sqn_acumulado = 0.0; double lucro_acumulado = 0.0; double quantidade_negocios = 0.0; int sqn_n = 0; int mes = -1; uint primeiro_negocio = 0; uint total_negocios; //--- request the history of trades if(HistorySelect(0,TimeCurrent()) == false) return 0.0; total_negocios = HistoryDealsTotal(); //--- the average for each month is calculated for each trade for(uint i=primeiro_negocio; i < total_negocios; i++) { ulong ticket=0; //--- Select the required ticket to pick up data if((ticket=HistoryDealGetTicket(i))>0) { datetime time = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME); double lucro = HistoryDealGetDouble(ticket,DEAL_PROFIT); if(lucro == 0) { //--- If there is no result, move on to the next trade. continue; } if(eh_um_novo_mes(time, mes)) { //--- If we have trades, then we calculate sqn, otherwise it will be equal to zero... if(quantidade_negocios>0) { sqn_acumulado += sqn(primeiro_negocio, i, lucro_acumulado, quantidade_negocios); } //--- The calculated amount sqns is always updated! sqn_n++; primeiro_negocio=i; lucro_acumulado = 0.0; quantidade_negocios = 0; } lucro_acumulado += lucro; quantidade_negocios++; } } //--- when exiting "for", we can have undesired result if(quantidade_negocios>0) { sqn_acumulado += sqn(primeiro_negocio, total_negocios, lucro_acumulado, quantidade_negocios); sqn_n++; } //--- take the simple average of sqns return NormalizeDouble(sqn_acumulado / sqn_n, 2); }
我们来通览代码:
第一个函数计算一组交易的标准差。 此处,我们遵循 Van Tharp 的建议,并在计算标准差时包含所有交易。 不过,在最终公式(在下面的函数内)当中,我们将交易数量限制为 100 笔。 这样做是为了令结果不会因交易数量而失真,从而令其更加实用和有意义。
最后,我们有 sqn_mes 函数,它检查它是否开始新的月份,并积累上述函数所需的一些数据。 在该函数结束时,将计算运行模拟期间的平均月度 SQN。 此简要解释旨在阐述代码和每个函数的用途。 按照这种方法,您可以更好地理解 SQN 计算。
double SQN_TESTER_ON_TESTER() { PrintFormat("%G,%G,%G", rr_medio(), CPCIndex(), sqn_mes()); return NormalizeDouble(sqn_mes() * CPCIndex(), 5); }
在我们结束之前
在本文完结之前,我们回到 include 主题,看看如何避免重复错误。 假设我们有一段含有 OnTester 函数的智能系统代码,并且我们想要把指定文件 include。 它如下所示(忽略此示例中的 OnTester 内容)。
//+------------------------------------------------------------------+ double OnTester() { return __LINE__; } //+------------------------------------------------------------------+ #include "ARTICLE_METRICS.mq5"
该段代码将引发函数重复错误,因为在我们的 EA 代码中,以及包含文件中都有一个同名 OnTester 函数。 不过,我们可以重命名其中一个,使之成为两个定义,并在模拟机制中启用或禁用所用的函数。 请参阅下面的示例。
//+------------------------------------------------------------------+ #define OnTester disable //#define SQN_TESTER_ON_TESTER disable double OnTester() { return __LINE__; } #undef OnTester //+------------------------------------------------------------------+ #include "ARTICLE_METRICS.mq5"
在这种新形式中,我们就不会出现函数重复错误,因为定义会将 EA 代码中的函数名称从 OnTester 更改为 disable。 现在,如果我们注释掉第一个定义,并取消第二个定义的注释,则 ARTICLE_METRICS 文件中的函数将重命名为 disable,而智能系统文件中的函数仍为 OnTester。
这种方式似乎是一种相当简单的方法,可以在两个函数之间切换,而无需注释掉若干行代码。 尽管它更具侵入性,但我相信很多用户会考虑它。 另一个用户会考虑的事情在于是否需要将函数保留在 EA 当中,因为包含文件中已有一个函数,这可能会变得令人困惑。
结束语
我们已经到达本文的结尾,其中我们展示的智能系统模型带有随机操作。 我们以它为例来演示品质因数计算。 我们研究了两种可能的计算:Van Tharp 和 Sunny Harris。 此外,还示意了一个新引入的 “风险-回报” 关系因数。 我们还演示了如何使用 “includes” 轻松地在不同的可用函数之间切换。
如果您有任何问题或发现错误,请发表文章评论。 所讨论的智能系统代码和衡量度文件都附于文后 zip 中,供您研究。
您是否用过其它品质衡量度? 在这里发表评论分享! 非常感谢您阅读本文。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11373



