English Русский Español Deutsch 日本語 Português
preview
优化结果的可视化评估

优化结果的可视化评估

MetaTrader 5测试者 | 23 三月 2022, 14:55
1 376 0
Aleksandr Slavskii
Aleksandr Slavskii

概述

自定义优化准则为优化智能交易系统提供了非常方便的设置。 但如果我们需要检查多个条件,我们则应运行多个优化,而这可能需要花费大量时间。 更好的解决方案是能够在一次优化过程中测试多个自定义准则。 甚而,最好能够即时查看余额和净值图形。

有多种可视化选项总是好的。 我们的大脑通过眼睛接收的信息超过 80%。 故此,在本文中,我们将研究创建优化图形,及选择最优定制准则的方法。

我们还将看到如何利用网站上发表的文章和论坛评论,在几乎不了解 MQL5 的情况下创建所需的解决方案。


格式化任务

  1. 收集每次优化通测的数据。
  2. 为每次优化通测构建余额/净值图。
  3. 计算若干自定义优化准则。
  4. 按照自定义优化准则针对图形按升序进行排序。
  5. 显示所有自定义准则的最优结果。


问题解决步骤

由于我们必须修改智能交易系统的代码,然而我们要尽量减少这些修改。

  • 因此,整个数据收集代码将在单独的包含文件 SkrShotOpt.mqh 中实现,而自定义准则将在 CustomCriterion.mqh 文件中计算。
  • ScreenShotOptimization.mq5 屏幕截图将绘制图形,并保存屏幕截图。

因此,我们只需要在智能交易系统中添加几行代码。


1. 收集数据。 SkrShotOpt.mqh

净值的最大值和最小值将在 OnTick() 函数里写入。

   double _Equity = AccountInfoDouble(ACCOUNT_EQUITY);
   if(tempEquityMax < _Equity)
      tempEquityMax = _Equity;
   if(tempEquityMin > _Equity)
      tempEquityMin = _Equity;

为了避免每次即时报价来临时都要检查持仓变化,我们将在 OnTradeTransaction() 函数中跟踪持仓变化

void IsOnTradeTransaction(const MqlTradeTransaction & trans,
                          const MqlTradeRequest & request,
                          const MqlTradeResult & result)
  {
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
      if(HistoryDealSelect(trans.deal))
        {
         if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY)
            _deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
         if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL)
            if(_deal_entry == DEAL_ENTRY_IN || _deal_entry == DEAL_ENTRY_OUT || _deal_entry == DEAL_ENTRY_INOUT || _deal_entry == DEAL_ENTRY_OUT_BY)
               allowed = true;
        }
  }

当开仓成交的数量发生变化时,填写余额和净值数组。

   if(allowed) // if there was a trade
     {
      double accBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      double accEquity = AccountInfoDouble(ACCOUNT_EQUITY);

      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      balance[_size] = accBalance;

      if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) // if a new position appeared
         equity[_size] = accEquity;
      else // if position closed
        {
         if(changesB < accBalance)
            equity[_size] = tempEquityMin;
         else
            switch(s_view)
              {
               case  min_max_E:
                  equity[_size] = tempEquityMax;
                  break;
               default:
                  equity[_size] = tempEquityMin;
                  break;
              }
         tempEquityMax = accEquity;
         tempEquityMin = accEquity;
        }

      _size = _size + 1;
      changesPos = PositionsTotal();
      changesB = accBalance;
      _deal_entry = -1;
      allowed = false;
     }

含有帧数据的文件大小是有限的。 如果有多笔交易,文件大小会增加,处理起来会很困难。 因此,只应向其写入最必要的信息。

交易开始时,写入余额和净值数额:

  • 交易完成后,如果成交以亏损了结,则写入最大净值数额
  • 如果成交以盈利了结,则写入最少净值数额。 

因此,几乎所有成交都有四个数组值:开盘时的余额和净值、收盘时的余额和最大/最小净值。

也许会出现在一次即时报价期间,一笔持仓被平仓,然后另开一笔新仓的情况。 在此情况下,只会写入一笔持仓。 这不会影响图形的可视化,同时大大减少了数组。

 

将收集的数据保存到文件

收集可盈利的优化通测才有意义。 此参数在设置中实现,因此,如果您有需要,可以另外登记亏损通测。 至于前向通测,它们也都会有记录。

在每次测试结束时,会触发测试器事件,此时利用 FrameAdd() 函数,把收集到的数据将写入一个文件。 而测试器事件由 OnTester() 函数处理。

bool  FrameAdd( 
   const string  name,        // public name/tag
   long          id,          // public id 
   double        value,       // value
   const void&   data[]       // array of any type
   );

此处提供了一个关于如何使用 FrameAdd() 函数的详细且清晰的示例:https://www.mql5.com/ru/forum/11277/page4#comment_469771

由于 FrameAdd() 只能写入一个数组和一个数值,但除了余额和净值之外,传递 ENUM_STATISTICS 枚举的数值之一也很不错,数据将按顺序写入一个数组,而数组大小将写入所传递的 “value” 数值。

   if(id == 1)  // if it is a backward pass
     {
      // if profit % and the number of trades exceed those specified in the settings, the pass is written into the file
      if(TesterStatistics(STAT_PROFIT) / TesterStatistics(STAT_INITIAL_DEPOSIT) * 100 > _profit && TesterStatistics(STAT_TRADES) >= trades)
        {
         double TeSt[42]; // total number of elements in the ENUM_STATISTICS enumeration is 41
         IsRecordStat(TeSt); // writing testing statistics to the array
         IsCorrect(); // adjusting balance and equity arrays

         if(m_sort != none)
           {
            while((sort)size_sort != none)
               size_sort++;
            double LRB[], LRE[], coeff[];
            Coeff = Criterion(balance, equity, LRB, LRE, TeSt, coeff, 3);// calculating custom criterion
            ArrayInsert(balance, equity, _size + 1, 0);     // joining balance and equity arrays into one
            ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data
            FrameAdd(name, id, _size + 1, balance);         // write the frame into the file
           }
         else
           {
            ArrayInsert(balance, equity, _size + 1, 0);     // joining balance and equity arrays into one
            ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data
            FrameAdd(name, id, _size + 1, balance);         // write the frame into the file
           }
        }
     }

前向通测的处理方式与回测类似,但实际上它们是优化的推测。 这就是为什么只写入它们的余额和净值,而非写入 ENUM_STATISTICS 值。

如果在测试结束时仍有持仓,测试器会将其平仓。

这意味着,如果存储持仓数量的变量在测试结束时与实际不符,我们需要一笔虚拟成交了结(写入当前余额和净值)。

void IsCorrect()
  {
   if(changesPos > 0) // if there is an open position by the testing end time, it should be virtually closed as the tester will close such a position
     {
      _size++;
      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      if(balance[_size - 2] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE);
         switch(s_view)
           {
            case  min_max_E:
               equity[_size - 1] = tempEquityMax;
               break;
            default:
               equity[_size - 1] = tempEquityMin;
               break;
           }
        }
      else
        {
         balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE);
         equity[_size - 1] = tempEquityMin;
        }
      balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE);
      equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY);
     }
   else
     {
      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE);
      equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY);
     }
  }

数据写入在此处完成。


从文件中读取数据。 ScreenShotOptimization.mq5

优化之后,将在以下路径创建一个含有帧数据的文件:C:\Users\user name\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester。 文件名为 EA_name.symbol.timeframe.mqd。 优化之后不能立即访问该文件。 但如果您重新启动终端,可用常规文件函数访问该文件。

该文件可在 C:\Users\user name\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester 处找到。

   int count = 0;
   long search_handle = FileFindFirst("Tester\\*.mqd", FileName);
   do
     {
      if(FileName != "")
         count++;
      FileName = "Tester\\" + FileName;
     }
   while(FileFindNext(search_handle, FileName));
   FileFindClose(search_handle);

首先,读入结构。

FRAME Frame = {0};
FileReadStruct(handle, Frame);
struct FRAME
  {
   ulong             Pass;
   long              ID;
   short             String[64];
   double            Value;
   int               SizeOfArray;
   long              Tmp[2];

   void              GetArrayB(int handle, Data & m_FB)
     {
      ArrayFree(m_FB.Balance);
      FileReadArray(handle, m_FB.Balance, 0, (int)Value);
      ArrayFree(m_FB.Equity);
      FileReadArray(handle, m_FB.Equity, 0, (int)Value);
      ArrayFree(m_FB.TeSt);
      FileReadArray(handle, m_FB.TeSt, 0, (SizeOfArray / sizeof(m_FB.TeSt[0]) - (int)Value * 2));
     }
   void              GetArrayF(int handle, Data & m_FB, int size)
     {
      FileReadArray(handle, m_FB.Balance, size, (int)Value);
      FileReadArray(handle, m_FB.Equity, size, (int)Value);
     }
  };

在 FRAME 结构函数中,填充 Data 结构函数,并从中构造进一步的图表。

struct Data
  {
   ulong             Pass;
   long              id;
   int               size;
   double            Balance[];
   double            Equity[];
   double            LRegressB[];
   double            LRegressE[];
   double            coeff[];
   double            TeSt[];
  };
Data                 m_Data[];

由于绘制数千个屏幕截图非常耗时,因此在脚本设置中指定一个参数,以便在利润低于指定百分比时禁用保存屏幕截图。

含有帧数据的文件在循环中进行处理。 

为了绘制图形,我们需要一个数据数组。 因此,最先写入的是所有符合最低盈利百分比准则的回测结果。

然后迭代所有回测,并根据回测编号为它们选择相应的回测。 前向通测余额数组也会被添加到回测余额数组当中。

该解决方案可以绘制两种类型的图形。 其中一个类似于策略测试器中的图形,即前向通测从初始本金开始。

前向通测的第二种变体从回测结束时的资金额开始。 在这种情况下,回测利润值被添加到前向通测余额和净值中,并写入回测数组的末尾。

当然,这只在前向通测优化执行期间才能实现。

   int handle = FileOpen(FileName, FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_BIN);
   if(handle != INVALID_HANDLE)
     {
      FileSeek(handle, 260, SEEK_SET);

      while(Res && !IsStopped())
        {
         FRAME Frame = {0};
         // read from the file to the Frame structure
         Res = (FileReadStruct(handle, Frame) == sizeof(Frame));
         if(Res)
            if(Frame.ID == 1) // if it is a Backward pass, write data to the m_Data structure
              {
               ArrayResize(m_Data, size + 1);
               m_Data[size].Pass = Frame.Pass;
               m_Data[size].id = Frame.ID;
               m_Data[size].size = (int)Frame.Value;
               Frame.GetArrayB(handle, m_Data[size]);  // write data to the m_Data structure arrays
               // if profit of this pass corresponds to the input settings, immediately calculate optimization criteria
               if(m_Data[size].TeSt[STAT_PROFIT] / m_Data[size].TeSt[STAT_INITIAL_DEPOSIT] * 100 >= profitPersent)
                 {
                  Criterion(m_Data[size].Balance, m_Data[size].Equity, m_Data[size].LRegressB, m_Data[size].LRegressE, m_Data[size].TeSt, m_Data[size].coeff, m_lineR);
                  size++;
                 }
              }
            else  // if it is a Forward pass, write to the end of the m_Data data structures
               if(m_Forward != BackOnly) // if drawing of only Backward passes is not selected in settings
                  for(int i = 0; i < size; i++)
                    {
                     if(Frame.Pass == m_Data[i].Pass) // if Back and Forward pass numbers match
                       {
                        int m = 0;
                        if(m_Forward == Back_Next_Forward) // if selected drawing of Forward graph as a continuation of Backward
                          {
                           Frame.GetArrayF(handle, m_Data[i], m_Data[i].size - 1); // write data at the end of the the m_Data structure array, with a one-trade shift
                           for(int x = m_Data[i].size - 1; x < m_Data[i].size + (int)Frame.Value - 1; x++)
                             {
                              m_Data[i].Balance[x] = m_Data[i].Balance[x] + m_Data[i].TeSt[STAT_PROFIT]; //  add profit of the Backward test to the Forward pass
                              m_Data[i].Equity[x] = m_Data[i].Equity[x] + m_Data[i].TeSt[STAT_PROFIT];
                             }
                           m = 1;
                          }
                        else
                           Frame.GetArrayF(handle, m_Data[i], m_Data[i].size); // if drawing of a Forward pass from a starting balance is selected

                        m_Data[i].coeff[Forward_Trade] = (int)(Frame.Value / 2); // number of forward trades (not exact))
                        m_Data[i].coeff[Profit_Forward] = m_Data[i].Balance[m_Data[i].size + (int)Frame.Value - m - 1] - m_Data[i].Balance[m_Data[i].size - m];
                        break;
                       }
                     if(i == size - 1) // if no Backward is found for this Forward pass, move the file pointer to the end of writing
                        FileSeek(handle, Frame.SizeOfArray, SEEK_CUR); // of this frame as if we read array data from the file
                    }
        }
      FileClose(handle);
      //---


构建图形

图形绘制函数。

string _GraphPlot(double& y1[],
                  double& y2[],
                  double& LRegressB[],
                  double& LRegressE[],
                  double& coeff[],
                  double& TeSt[],
                  ulong pass)
  {
   CGraphic graphic;
//--- create graphic
   bool res = false;
   if(ObjectFind(0, "Graphic") >= 0)
      res = graphic.Attach(0, "Graphic");
   else
      res = graphic.Create(0, "Graphic", 0, 0, 0, _width, _height);

   if(!res)
      return(NULL);

   graphic.BackgroundMain(FolderName);  // print the Expert Advisor name
   graphic.BackgroundMainSize(FontSet + 1); // font size for the Expert Advisor name

   graphic.IndentLeft(FontSet);
   graphic.HistoryNameSize(FontSet); // font size for the line names
   graphic.HistorySymbolSize(FontSet);

   graphic.XAxis().Name("pass " + IntegerToString(pass)); // show the pass number along the X axis
   graphic.XAxis().NameSize(FontSet + 1);

   graphic.XAxis().ValuesSize(12); // price font size
   graphic.YAxis().ValuesSize(12);

//--- add curves
   CCurve *curve = graphic.CurveAdd(y1, ColorToARGB(clrBlue), CURVE_POINTS_AND_LINES, "Balance"); // plot the balance graph
   curve.LinesWidth(widthL);  // graph line width
   curve.PointsSize(widthL + 1); // size of dots on the balance graph

   CCurve *curve1 = graphic.CurveAdd(y2, ColorToARGB(clrGreen), CURVE_LINES, "Equity");  // plot the equity graph
   curve1.LinesWidth(widthL);

   int size = 0;
   switch(m_lineR) // plot the regression line
     {
      case  lineR_Balance: // balance regression line
        {
         size = ArraySize(LRegressB);
         CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
         curve2.LinesWidth(widthL);
        }
      break;
      case  lineR_Equity: // equity regression line
        {
         size = ArraySize(LRegressE);
         CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
         curve2.LinesWidth(widthL);
        }
      break;
      case  lineR_BalanceEquity: // balance and equity regression line
        {
         size = ArraySize(LRegressB);
         CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
         curve2.LinesWidth(widthL);

         CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
         curve2.LinesWidth(widthL);
        }
      break;
      default:
         break;
     }
//--- plot curves
   graphic.CurvePlotAll();

// Important!!!  All lines and captions must be created after creating the graph; otherwise, the graph will override them

   if(size == 0)
     {
      size = ArraySize(LRegressE);
      if(size == 0)
         size = ArraySize(LRegressB);
     }

   int x1 = graphic.ScaleX(size - 1); //Scales the value of the number of trades along the X axis
   graphic.LineAdd(x1, 30, x1, _height - 45, ColorToARGB(clrBlue), LINE_END_BUTT); // construct the vertical line denoting the end of the Backward period

   string txt = "";
   int txt_x = 70;// text indent along the X axis
   int txt_y = 30;// text indent along the Y axis

   graphic.FontSet("Arial", FontSet);// Set current font parameters

   for(int i = 0; i < size_sort; i++)  // Write all coefficients and criteria on the chart
     {
      if(coeff[i] == 0)
         continue;
      if(i == 1 || i == 3)
         txt = StringFormat("%s = %d", EnumToString((sort)i), (int)coeff[i]);
      else
         if(i == 0 || i == 2)
            txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]);
         else
            txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]);
      graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen));
     }

   txt_y = txt_y + FontSet * (size_sort - 1);
   txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]);
   graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen));
   txt = StringFormat("Expected  payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]);
   graphic.TextAdd(txt_x, txt_y + FontSet * 2, txt, ColorToARGB(clrGreen));

   graphic.Update();
//--- return resource name
   return graphic.ChartObjectName();
  }


您可以从以下文章中了解有关如何使用 CGraphic 的更多信息:


图表截图保存到文件目录下的单独文件夹当中。 文件夹名为 EA_name.symbol.timeframe。

bool BitmapObjectToFile(const string ObjName, const string _FileName, const bool FullImage = true)
  {
   if(ObjName == "")
      return(true);

   const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(0, ObjName, OBJPROP_TYPE);
   bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP);

   if(Res)
     {
      const string Name = __FUNCTION__ + (string)MathRand();

      ObjectCreate(0, Name, OBJ_CHART, 0, 0, 0);
      ObjectSetInteger(0, Name, OBJPROP_XDISTANCE, -5e3);

      const long chart = ObjectGetInteger(0, Name, OBJPROP_CHART_ID);

      Res = ChartSetInteger(chart, CHART_SHOW, false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, 0, 0, 0) &&
            ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(0, ObjName, OBJPROP_BMPFILE)) &&
            (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(0, ObjName, OBJPROP_XSIZE)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(0, ObjName, OBJPROP_YSIZE)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_XOFFSET)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_YOFFSET)))) &&
            ChartScreenShot(chart, FolderName + "\\" + _FileName, (int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE));
      ObjectDelete(0, Name);
     }

   return(Res);
  }


这些是结果图形。

文件夹中的图形。

如果选择了“保存所有屏幕截图”,则屏幕截图名称由自定义准则组成,该准则的择选是:排序+利润+通测编号。 

如果只选择了最佳通测结果,屏幕截图名称由“自定义准则+利润”组成。

此处是脚本创建的图形的模样。


下面是来自策略测试器的相同图形


我在这里展示了非常相似的图表,但在大多数情况下,它们会有所不同。 这是因为在策略测试器中,沿 X 轴的成交与时间有关,而脚本绘制的图形中,X 轴与交易数量有关。 此外,因为我们必须在一个帧数据中写入最少的信息,从而保证文件足够小,而由脚本创建的图形其净值额不足以进行分析。 与此同时,这些数据足以对优化通测的效率进行初步评估。 它也足以计算自定义优化准则。

优化之后,在运行 ScreenShotOptimization 之前,请重启终端!

最初,我只想可视化所有优化图形。 但当我实现了这个脚本,并在文件夹中看到了 7000 个屏幕截图时,我才明白:不可能处理如此多的图形。 取而代之,我们需要根据某些准则来筛选其中最佳的。

我很久以前就注意到算法交易者分为两类:

  1. 他们中的一些人认为,EA 应该在一个非常大的时间区间内进行优化,相当于几年甚至几十年,然后 EA 就会完美工作。
  2. 其他人则认为 EA 必须定期重新优化,选用一个小的时间区间,例如:一个优化月 + 一个交易周;或三个优化月 + 一个交易月;或任何其它合适的重新优化时间表。

我属于第二种类型。

这就是为什么我决定搜索优化准则,并将其作为一个过滤器来筛选最佳通测结果。


创建自定义优化准则

所有自定义优化准则将在单独的包含文件 CustomCriterion.mqh 中计算,因为这些计算最终将即用于绘制图形的脚本操作,以及我们进行优化的智能交易系统。

在创建自己的自定义优化准则之前,我查阅了很多相关资料。

利用 R 平方评估策略余额曲线品质

该文章详细介绍了线性回归,及其利用 AlgLib 库进行的计算。 它还对 R^2 的确定系数,及其在测试结果中的应用进行了很好的描述。 我建议大家阅读这篇文章。

用于计算线性回归 R^2 和盈利能力的函数:

void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c)
  {
//-- Fill the matrix: Y - Array value, X - ordinal number of the value
   CMatrixDouble xy(total, 2);
   for(int i = 0; i < total; i++)
     {
      xy[i].Set(0, i);
      xy[i].Set(1, Array[i]);
     }

//-- Find coefficients a and b of the linear model y = a*x + b;
   int retcode = 0;
   double a, b;
   CLinReg::LRLine(xy, total, retcode, b, a);

//-- Generate the linear regression values for each X;
   ArrayResize(LR, total);
   for(int x = 0; x < total; x++)
      LR[x] = x * a + b;

   if(m_calc == c)
     {
      //-- Find the coefficient of correlation of values with their linear regression
      corr = CAlglib::PearsonCorr2(Array, LR);

      //-- Find R^2 and its sign
      coeff[r2] = MathPow(corr, 2.0);
      int sign = 1;
      if(Array[0] > Array[total - 1])
         sign = -1;
      coeff[r2] *= sign;

      //-- Find LR Standard Error
      if(total - 2 == 0)
         stand_err = 0;
      else
        {
         for(int i = 0; i < total; i++)
           {
            double delta = MathAbs(Array[i] - LR[i]);
            stand_err =  stand_err + delta * delta;
           }
         stand_err = MathSqrt(stand_err / (total - 2));
        }
     }
//-- Find ProfitStability = Profit_LR/stand_err
   if(stand_err == 0)
      coeff[ProfitStability] = 0;
   else
      coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err;
  }


基于余额值的优化策略,将其结果与“余额 + 最大锋锐比率” 准则进行比较。

从这篇文章中,我得到了 ProfitStability(利润稳定性)自定义优化准则。 它的计算很简单:首先,我们计算 LR 标准误差,即,回归线与余额或净值线的平均偏差。 然后,从回归线的结果值中扣除起始值,来获得 TrendProfit(趋势利润)

ProfitStability(利润稳定性)的计算为 TrendProfit(趋势利润)LR 标准误差 的比率:

本文详细描述了该优化准则的所有优点和缺点。 它还提供了大量测试,并取 ProfitStability(利润稳定性)与其它优化准则进行比较。

由于线性回归既可以计算余额,也可以计算净值,并且盈利能力与线性回归计算绑定,因此盈利能力的计算能够在线性回归计算函数中实现。


创建智能交易系统的自定义准则

这是于 2011 年发表的一篇相当古老的文章,但它很有趣,而且仍然相关。 我在本文中使用了一个计算交易系统安全系数(Trading System Safety Factor - TSSF)的公式。

TSSF = Avg.Win / Avg.Loss ((110% - %Win) / (%Win-10%) + 1)

   if(TeSt[STAT_PROFIT_TRADES] == 0 || TeSt[STAT_LOSS_TRADES] == 0 || TeSt[STAT_TRADES] == 0)
      coeff[TSSF] = 0;
   else
     {
      double  avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES];
      double  avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES];
      double  win_perc = 100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES];
      //  Calculate the secure ratio for this percentage of profitable deals:
      if((win_perc - 10.0) + 1.0 == 0)
         coeff[TSSF] = 0;
      else
        {
         double  teor = (110.0 - win_perc) / (win_perc - 10.0) + 1.0;
         //  Calculate the real ratio:
         double  real = avg_win / avg_loss;
         if(teor != 0)
            coeff[TSSF] = real / teor;
         else
            coeff[TSSF] = 0;
        }
     }


开发和分析交易系统的优化方法

在本文中,我采用用了 LinearFactor,计算如下:

  • LinearFactor = MaxDeviation/EndBalance
  • MaxDeviaton = Max(MathAbs(Balance[i]-AverageLine))
  • AverageLine=StartBalance+K*i
  • K=(EndBalance-StartBalance)/n
  • n - 测试中的成交数量

有关详细信息,请参阅上述文章。 这篇文章很有趣,它提供了许多有用的东西。

展望未来,我要说的是,我没办法找到一个适合任何智能交易系统的通用自定义优化准则。 针对不同的智能交易系统,采用不同的准则,才能提供最佳结果。

在某些 EA 当中,LinearFactor 有极好的效果。

   double MaxDeviaton = 0;
   double K = (Balance[total - 1] - Balance[0]) / total;
   for(int i = 0; i < total; i++)
     {
      if(i == 0)
         MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i));
      else
         if(MathAbs(Balance[i] - (Balance[0] + K * i) > MaxDeviaton))
            MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i));
     }
   if(MaxDeviaton ==0 || Balance[0] == 0)
      coeff[LinearFactor] = 0;
   else
      coeff[LinearFactor] = 1 / (MaxDeviaton / Balance[0]);

在个人通信中,作者提到这个准则可以进一步增强,他解释了如何加强,但我无法在代码中实现这些技巧。


因此,我在代码中添加了四个自定义优化准则。

  1. R^2 - 判定系数。
  2. ProfitStability(利润稳定性)。
  3. TSSF - 交易系统安全系数(Trading System Safety Factor)
  4. LinearFactor(线性系数)。

所有这些优化准则都在我们的项目中。

遗憾的是,我无法添加 “Complex Criterion max(复杂标准 max)”,因为我无法找到它的计算方法。 


我的优化准则

基于所有这些文章,我们可以继续创建自己的优化准则。

我们打算看哪个余额图形? 当然,一条理想的曲线应该是一条充满自信的直线。

我们来看一张有利润的图形。



在优化过程中,我们不能比较几个 EA 的结果,但这些都是相同 EA 的结果,这就是为什么我决定不考虑 EA 生成盈利的时间。

此外,我不考虑交易量。 但是,如果手数是动态计算的,则有必要以某种方式在自定义标准中计算交易量(这未实现)。

有多少交易? 我并不在乎有多少交易会为我带来上千盈利,一次或数百次交易,这就是为什么我也忽略了它们的数量。 不过,请注意,如果交易样本太少,线性回归的计算不能保证正确性。

在这个图形中什么是最重要的?首先,它是盈利。 我决定评估相对盈利,即,相对于初始余额的利润。

Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];

另一个非常重要的参数是 drawdown(回撤)。 

测试其中的回撤是如何计算的? 将左侧的最大净值额与右侧的最小净值额进行比较。


高于余额的数额是相当令人不爽的 — 实际上这笔钱我们并未赚到。 但当数额低于余额时,这真的很令人痛苦。

对我来说,低于余额的最大回撤是最重要的。 因此,交易不应该造成太大伤害。


double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total)
  {
   if(TeSt[STAT_INITIAL_DEPOSIT] == 0)
      return(0);

   double Balance_max = Balance[0];
   double Equity_min = Equity[0];
   difference_B_E = 0;
   double Max_Balance = 0;

   switch((int)TeSt[41])
     {
      case  0:
         difference_B_E = TeSt[STAT_EQUITY_DD];
         break;
      default:
         for(int i = 0; i < total - 1; i++)
           {
            if(Balance_max < Balance[i])
               Balance_max = Balance[i];
            if(Balance[i] == 10963)
               Sleep(1);
            if(Balance_max - Equity[i + 1] > difference_B_E)
              {
               Equity_min = Equity[i + 1];
               difference_B_E = Balance_max - Equity_min;
               Max_Balance = Balance_max;
              }
           }
         break;
     }

   return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]);
  }

因为自定义准则值应该按升序考虑,我从一个准则值中扣除了回撤。 因此,值越高,回撤幅度越小。

我称其为最终数值(equity_rel),即,相对于初始余额的回撤

事实证明,为了正确计算 equity_rel,以前所用的净值采集方法并不适用。 由于一些最低的净值数额丢失,我不得不用两个变量来实现保存净值数额。 第一种选项是当亏损结束时,保存最大净值;在盈利结束时,保存最小净值。 第二个操作只保存最小净值。

为了告知脚本所用的净值采集方法,这些选项已写入测试统计数组 TeSt[41]。 此外,在 EquityDD() 函数中,我们根据净值采集方法计算 equity_rel 和 difference_B_E。

//---

  接下来,我决定合并不同的数据,并检查结果。

//---

基于 equity_rel,就能够计算和替代挽回因子。 

difference_B_E — 按货币计算的最大净值回撤。

coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;

为了令曲线更接近于一条直线,我在第二个替代挽回因子中加入了 R^2

coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];

由于设置允许基于余额或净值选择相关性计算,因此如果我们只记录最小净值数额,R^2 将与回撤相关。

自定义准则 “相对利润 * R^2” 公式可以生成有趣的结果。

coeff[profit_r2] = relative_prof * coeff[r2];

它在研究相关性有多大时也很有用。 因此,下一个自定义准则如下所示。

相对盈利 *R^2 / 标准误差

   if(stand_err == 0)
      coeff[profit_r2_Err] = 0;
   else
      coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;

现在我们有了相对利润,相对于初始余额的净值回撤和 R^2,我们可以创建一个公式,参考了利润、回撤、和曲线与直线的接近度

relative_prof + equity_rel + r2;

如果我们想让这些参数变得更重要呢? 因此,我添加了权重变量 “ratio”。 

现在我们还有三个自定义优化准则。

coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2];

coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2];

coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;


我们总共有 12 个自定义优化准则。

1. R^2 - 判定系数

2.  ProfitStability

3. TSSF - 交易系统安全系数

4.  LinearFactor

5.  equity_rel  

6. c_recovery_factor

7. c_recovery_factor_r2

8. profit_r2

9. profit_r2_Err

10. profit_R_equity_r2

11. profit_equity_R_r2

12. profit_equity_r2_R


检查结果

为了检查结果,我们需要创建一个简单的智能交易系统...

根据临时文章计划,此处是一个简单的智能交易系统代码。 不幸的是,两个简单的 EA 均未展现出预期的结果。

因此,我不得不选择一个创建订单的 EA,并使用它显示结果(我隐藏了 EA 的名称)。

假设现在是四月底,我们计划在真实账户上启动 EA。 如何找出优化准则,令其能够交易获利?

我们启动一个为期三个月的前向优化。  



优化之后重启终端。

运行脚本,仅在设置中选择最佳结果。 以下是文件夹中的结果。


然后,我依视觉从所有结果中选择最好的前向通测。 我有几个类似的结果,所以我选择了 profit_equity_R_r2,因为在这个优化中,优先考虑的是较低的回撤。


在策略测试器的同一时期如下所示:


以下是用于比较的最大余额:


以下是最复杂的准则:


正如您所看到的,与最大余额和最大复杂度相比,基于最佳 profit_equity_R_r2 通测的情况下,图表上的交易要少得多,利润大致相同,然而图形要平滑得多。


因此,我们确定了自定义准则:profit_equity_R_r2。 现在,我们来看看如果我们在过去的三个月里运行优化,并且在优化过程中得到了最佳设置,并决定在五月份切换到这个设置,会发生什么。

我们运行前向优化,并检查。

优化设置。  


在 EA 设置中,设置执行优化的自定义准则。

由此,如果我们在过去三个月里基于 profit_equity_R_r2 自定义准则优化 EA,

之后在 4 月 1 日至 5 月 1 日期间,以获得的设置进行交易,我们将赚取 750 个单位,净值回撤 300 个单位。



现在,我们检查一下 fxsaber 开发的 Validate EA 的性能!

我们看看 EA 在四个月里是如何交易的。 验证设置:优化三个月,并交易一个月。


如您所见,EA 经受住了这次压力测试!

我们将其与拥有相同设置,但基于最大复杂准则优化的图表进行比较。



EA 幸存下来,但是...


结束语

优势:

  1. 您可以同步查看优化结果的所有图形。
  2. 能为 EA 找到最佳自定义优化准则。

缺点:

由于记录数据的限制,这些图形的信息量比策略测试器中的图形要少。

随着成交大量进行,含有帧数据的文件会大幅增长,变得无法读取。

//---

正如实验所表明的,没有独占矛头的超级准则:不同的准则为不同的智能交易系统提供最佳结果。

但我们已拥有一整套这样的准则。 如果读者支持这个思路,选择的范围会更广。

//---

其中一位测试人员建议在文章中讲述脚本设置,如此这般,不愿意学习材料的用户就可以直接使用代码,而不必研究细节。


如何使用

为了使用此代码,请下载下面附带的 zip 文件,将其解压缩,并复制到 MQL5 文件夹。

在终端里,选择 “文件 -> 打开数据文件夹 -> 在新文件夹的空白处点击鼠标右键 -> 插入"。 如果出现提示询问您是否要替换目标文件夹中的文件,请选择“替换”。

接下来,运行 MetaEditor,在其中打开 EA,并进行以下修改:

1.  在 OnTick() 函数里插入 IsOnTick();

 2. 在 EA 底部添加以下代码:

  #include <SkrShotOpt.mqh>     

  double OnTester() {return(IsOnTester());}

  void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request,const MqlTradeResult & result) 
    {
      IsOnTradeTransaction(trans, request, result);
     }
 如果 EA 已经有了 OnTradeTransaction() 函数,则在其中添加 IsOnTradeTransaction(trans, request, result);

3. 按下“编译”。 

如果有错误生成,通知与匹配变量名相关,则需要更改名称。


设置

一旦您插入代码之后,EA 设置中将显示另外几行。

不要勾选这些选项框来优化这些设置!!!这些设置不会影响优化结果,因此不要对其进行优化


  • Write the pass if trades more than:如果您的 EA 执行了太多交易,则可以增加此参数,从而减少写入帧数据文件的数据量。
  • Write the pass if profit exceeds % — 默认情况下,亏损通测结果将被删除。 如果您你不想看到利润低于初始余额的某个百分比,您可以改变它。
  • Select equity values — 如果需要正确计算以下自定义准则,请设置 "save only min equity",quity_rel,c_recovery_factor,c_recovery_factor_r2,profit_r2,profit_r2_Err,profit_R_equity_r2,profit_equity_R_r2,profit_equity_r2_R
    如果您想拥有与策略测试器中类似的图形,请选择 “save min and max equity”

  • Custom criterion — 如果其设置为 “none”,则会将帧记录到文件中,但不会计算自定义准则(不会增加优化时间),

但在这种情况下,您不能按自定义准则选择优化。 此外,以下所有参数都不会产生任何影响。

如果要按自定义准则运行优化,应在此参数中选择一些自定义准则。 

不要忘记,自定义准则的计算取决于参数 “select equity values” 和 “calculate criterion by”

  • Calculate criterion by - 根据余额或净值计算 R^2;影响使用 R^2 的所有自定义准则
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R

    //----

    脚本设置。


    • Draw regression line:选择要绘制的回归线类型 - 余额、净值、余额和净值、无。
    • Profit percent more than - 打印屏幕截图需要大量时间,因此您可以选择只打印利润超过参数的屏幕截图。
    • Only best results - 如果为 true,则只保存含有每个自定义准则最佳结果的屏幕截图;否则会保存所有。
    • Custom criterion - 如果选择了所有屏幕截图,此参数可用于设置自定义准则,根据该准则对文件夹中的屏幕截图进行排序。
    • ratio - 计算自定义准则的权重 profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
    • Calculate criterion by - 根据余额或净值计算 R^2;影响使用 R^2 的所有自定义准则

             r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.

    • Graph - 在测试器中的 “Back separately from Forward” 图形之间进行选择,即前向通测以初始余额开始,  

    或 "Back continued by Forward" - 前向通测从回测的最后结束余额开始。

    //---

    所有在该网站上发表的文章对编写本程序都非常有帮助。

    我要感谢本文提到的所有文章的作者!


    本文由MetaQuotes Ltd译自俄文
    原文地址: https://www.mql5.com/ru/articles/9922

    附加的文件 |
    SkrShotOpt.mqh (17.56 KB)
    MQL5.zip (11.59 KB)
    DoEasy 函数库中的图形(第九十二部分):标准图形对象记忆类。 对象属性变更历史记录 DoEasy 函数库中的图形(第九十二部分):标准图形对象记忆类。 对象属性变更历史记录
    在本文中,我将创建标准图形对象记忆类,能够在对象修改其属性时保存其过往状态。 反之,这样就能够溯源以前的图形对象状态。
    从头开始开发一款智能交易系统 从头开始开发一款智能交易系统
    在本文中,我们将讨论如何做到最少编程来开发一款交易机器人。
    从市场里选择智能交易系统的正确途径 从市场里选择智能交易系统的正确途径
    在本文中,我们将研究购买智能交易系统时应该注意的一些要点。 我们还将寻求提升盈利的方法,从而明智地花钱,并从付出中获取盈利。 此外,读完本文之后,您会发现,即便使用简单免费的产品也有可能赚到钱。
    DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录 DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录
    在本文中,我将改进基本功能,从而能够基于函数库程序来控制图形对象事件。 我一开始将以“对象名称”属性为例,实现存储图形对象更改历史的功能。