English Русский Español Deutsch 日本語 Português
preview
为智能系统制定品质因数

为智能系统制定品质因数

MetaTrader 5示例 | 8 一月 2024, 17:11
348 1
Ricardo Rodrigues Lucca
Ricardo Rodrigues Lucca

概述

在本文中,我们将见识到如何制定一个品质得分,并由您的智能系统从策略测试器返回。 在下面的图例 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 所示的结果。

EA 针对 USDJPY,在 2023-01-01 至 2023-05-19 期间,OHLC 模式,H1 执行。

图例 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 计算。

OnTester 函数可以打印所有三个值,并且可以在测试器选项卡中查询或保存到文件中,甚或我们能够返回几个数值相乘的结果,如此它出现在报告之中,如下所示。
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

附加的文件 |
ARTICLE.zip (4.73 KB)
ARTICLE_METRICS.mq5 (10.33 KB)
ARTICLE_MT5.mq5 (11.87 KB)
最近评论 | 前往讨论 (1)
Feng Chen
Feng Chen | 10 1月 2024 在 13:18
太好了,刚好需要这个教程
开发回放系统 — 市场模拟(第 14 部分):模拟器的诞生(IV) 开发回放系统 — 市场模拟(第 14 部分):模拟器的诞生(IV)
在本文中,我们将继续探讨模拟器开发的新阶段。 这次,我们会见到如何有效地创建随机游走类型的走势。 这种类型的走势非常引人入胜,因为它是构成资本市场上所发生一切的基础。 此外,我们将开始了解一些对于进行市场分析至关重要的概念。
开发回放系统 — 市场模拟(第 13 部分):模拟器的诞生(III) 开发回放系统 — 市场模拟(第 13 部分):模拟器的诞生(III)
为了下一阶段的工作,我们将于此简化一些与操作相关的元素。 我还会解释如何让您把模拟器随机生成的内容可视化。
利用 MQL5 的交互式 GUI 改进您的交易图表(第一部分):可移动 GUI(I) 利用 MQL5 的交互式 GUI 改进您的交易图表(第一部分):可移动 GUI(I)
凭借我们的利用 MQL5 创建可移动 GUI 的综合指南,令您的交易策略或实用程序焕发出呈现动态数据的力量。 深入了解图表事件的核心概念,并学习如何在同一图表上设计和实现简单、多个可移动的 GUI。 本文还探讨了往 GUI 上添加元素的过程,从而增强其功能和美观性。
MQL5 中的范畴论 (第 9 部分):幺半群(Monoid)— 动作 MQL5 中的范畴论 (第 9 部分):幺半群(Monoid)— 动作
本文是以 MQL5 实现范畴论系列的延续。 在这里,我们继续将“幺半群 — 动作”当为幺半群变换的一种手段,如上一篇文章所涵盖的内容,从而增加了应用。