English Русский 中文 Español Deutsch Português
preview
最適化結果の視覚的評価

最適化結果の視覚的評価

MetaTrader 5テスター | 10 3月 2022, 09:57
994 0
Aleksandr Slavskii
Aleksandr Slavskii

はじめに

カスタム最適化基準は、エキスパートアドバイザーを最適化するための非常に便利なセットアップを提供します。ただし、複数の基準を確認する必要がある場合は複数の最適化を実行する必要もあり、これには時間がかかる可能性があります。より良い解決策は、1回の最適化中に複数のカスタム基準をテストできるようにすることです。さらに、バランスとエクイティのグラフをすぐに表示できると便利です。

さまざまな視覚化オプションがあることは常に良いことです。私たちの脳は、目を通して情報の80パーセント以上を受け取ります。したがって、この記事では、最適化グラフの作成と、最適なカスタム基準を選択する方法について検討します。

また、Webサイトに公開されている記事とフォーラムのコメントを使用して、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;
     }

フレーム付きのファイルのサイズには制限があります。取引が多いとファイルサイズが大きくなり、処理が難しくなるため、最も必要な情報のみを書き込みます。

取引が開始されたら、バランスとエクイティの値を書き込みます。

  • 取引が損失を伴って成立した場合は、成約時に最大エクイティ値を記入
  • 取引が利益で成立した場合は、最小エクイティ値を記入 

したがって、ほとんどすべての取引には、配列で記述された4つの値があります。開始時のバランスとエクイティ、終了時のバランスと最大/最小エクイティです。

同じティックで1つのポジションが決済されて、別のポジションが開く場合があります。この場合、1つのポジションのみが書き込まれます。これは、配列を大幅に削減しながら、グラフの視覚化には影響しません。

 

収集したデータをファイルに保存する

有益な最適化パスを収集することだけが理にかなっています。このパラメータは設定に実装されているため、必要に応じて、損失を出すパスを追加で登録できます。フォワードパスについては、すべて記録されています。

FrameAdd()関数を使用すると、収集されたデータは、テスターイベント時に、各シングルパスの最後にファイルに書き込まれます。次に、Testerイベントは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()は1つの配列と1つの数値「value」のみを書き込むことができるため、バランスとエクイティに加えて、ENUM_STATISTICS列挙型のすべての値を渡すことをお勧めします。データは1つの配列に順番に書き込まれ 、配列サイズは渡された「値」の数値に書き込まれます。

   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構造体関数では、データ構造体関数が入力され、そこからさらにチャートが作成されます。

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

何千ものスクリーンショットの描画には非常に時間がかかるため、スクリプト設定で、利益が指定された割合未満の場合にスクリーンショットの保存を無効にするパラメータを指定します。

フレームを含むファイルはループで処理されます。 

グラフを描くには、1つのデータ配列が必要です。したがって、最小利益率の基準を満たすすべてのバックワードパスが最初に書き込まれます。

次に、すべてのバックワードパスが繰り返され、パス番号に従って、関連するフォワードパスが選択されます。フォワードパスバランス配列がバックワードパスバランス配列に追加されます。

このソリューションでは、2種類のグラフを描画できます。それらの1つは、ストラテジーテスターのグラフに似ています。つまり、フォワードパスは初期デポジットから始まります。

フォワードパスの2番目のバリエーションは、バックワードパスが終了したデポジットから始まります。この場合、バックワードパスの利益値はフォワードパスのバランスとエクイティに追加され、バックワードパスの配列の最後に書き込まれます。

もちろん、これは、最適化が転送期間で行なわれた場合にのみ実行されます。

   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);
      //---


Constructing Graphs

Graph plotting function.

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の操作方法について詳しくは、次の記事をご覧ください。


チャートのスクリーンショットは、Filesディレクトリの下の別のフォルダに保存されます。フォルダ名は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個のスクリーンショットを見たとき、これほど多くのグラフを操作することは不可能であることがわかりました。代わりに、特定の基準に基づいてそれらの最良のものを選択する必要があります。

ずっと前に気づいたのですが、アルゴリズムトレーダーは2つのカテゴリに分類されています。

  1. その一部は、EAは数年または数十年に相当する非常に長い期間で最適化されるべきであり、その後EAが機能すると信じています。
  2. あと一部は、EAを定期的に再最適化する必要があると考えます。たとえば、一ヶ月の最適化+1週の取引、3ヶ月の最適化+1月の取引、またはその他の適切な再最適化スケジュールです。

私は2番目のタイプです。

そのため、最適なパスを選択するためのフィルタとして機能する最適化基準を検索することにしました。


カスタム最適化基準の作成

すべてのカスタム最適化基準は、別のインクルードファイル(CustomCriterion.mqh)で計算されます。これらの計算は、グラフを描画するためのスクリプト操作と、最適化するエキスパートアドバイザーの両方で使用されるためです。

独自のカスタム最適化基準を作成する前に、関連する資料をたくさん見つけました。

戦略バランス曲線の品質評価としての R 乗

この記事では、線形回帰とAlgLibライブラリを使用したその計算について詳しく説明しています。また、R ^ 2決定係数とそのテスト結果への応用についての適切な説明も含まれています。読むことをお勧めします。

次は、線形回帰、R^2、ProfitStabilityを計算するための関数です。

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は、TrendProfitLR標準誤差の比率として計算されます。

この記事では、この最適化基準のすべての長所と短所について詳しく説明しています。また、ProfitStabilityを他の最適化基準と比較するための多くのテストも備えています。

線形回帰はバランスとエクイティの両方で計算でき、ProfitStabilityは線形回帰計算にバインドされているため、ProfitStabilityの計算は線形回帰計算関数に実装されます。


EXPERT ADVISORS最適化のカスタム基準作成

これは2011年に書き戻されたかなり古い記事ですが、興味深いものであり、依然として関連性があります。この記事から取引システムの安全率(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]);

個人的なやり取りの中で、著者はこの基準をさらに強化できると述べ、その方法を説明しましたが、これらのヒントをコードに実装することはできませんでした。


そこで、4つのカスタム最適化基準をコードに追加しました。

  1. R^2 - 決定係数
  2. ProfitStability
  3. TSSF - 取引システムの安全率
  4. LinearFactor

これらの最適化基準はすべて私たちのプロジェクトにあります。

「Complex Criterion max」の計算方法がわからなかったため、追加できなかったのは残念です。 


私の最適化基準

これらすべての記事に基づいて、独自の最適化基準の作成に進むことができます。

どのバランスグラフを見たいでしょうか。もちろん、理想的なのは自信を持って成長する直線です。

利益を示すグラフを見てみましょう。



最適化では、複数のEAの結果を比較しませんが、これらは同じEAの結果であるため、EAが利益を生み出した時間を考慮しないことにしました。

また、ボリュームは考慮していません。ただし、ロットが動的に計算される場合は、カスタム基準計算にボリュームを含める必要があります(これは実装されていません)。

取引数はどうでしょうか。1000ドルを1回の取引で得られるか100回の取引で得られるのかはどうでもいいので、これらの数を無視します。ただし、取引が少なすぎると、線形回帰が正しく計算されません。

このグラフで重要なことは何でしょうか。まず第一に、それは利益です。相対的な利益、つまり開始残高に対する利益を評価することにしました。

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

もう1つの非常に重要なパラメータは、ドローダウンです。 

テスターでドローダウンはどのように計算されるのでしょうか。左側の最大エクイティは、右側の最小エクイティ値と比較されます。


バランスを超える値はかなり不快です。それは私たちが稼ぐことができなかったお金です。しかし、値がバランスを下回っているときは本当に苦痛です。

私にとって、バランスの下の最大ドローダウンが最も重要です。ですから、取引はそれほど害にはならないはずです。


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]);
  }

カスタム基準値は昇順で検討する必要があるため、1からドローダウンを減算しました。値が高いほど、ドローダウンは低くなります。

結果の値をequity_relと呼びました。つまり、開始残高に対するドローダウンです。

Equity_relを正しく計算するには、以前に使用されていたエクイティ収集方法は適切ではないことが判明しました。最小のエクイティ値の一部が失われるため、エクイティ値を保存するために2つのバリアントを実装する必要がありました。最初のオプションでは、決済に損失が伴ったときにはエクイティの最大値、利益が伴ったときには最小値を保存します。2番目の操作では、エクイティの最小値のみを保存します。

使用されたエクイティ収集方法についてスクリプトに通知するために、これらのオプションはテスター統計量TeSt[41]を使用して配列に書き込まれています。さらに、EquityDD()関数では、エクイティ収集方法に従って、equity_relとdifference_B_Eを計算します。

//---

  次に、さまざまなデータを組み合わせて結果を確認することにしました。

//---

equity_relに基づいて、代替の回収率を計算することができます。 

difference_B_E - 金銭面での最大エクイティドローダウン。

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

グラフを直線に近づけるために、2番目の代替回復係数に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」を追加しました。 

これで、さらに3つのカスタム最適化基準があります。

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.変化とトレンドを伴うEURUSD クオート(定数)。 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


結果の確認

結果を確認するには、簡単なエキスパートアドバイザーを作成する必要があります。

この記事の暫定的な計画では、ここに簡単なエキスパートアドバイザーコードがあるはずですでした。残念ながら、2つの単純なEAは望ましい結果を示しませんでした。

したがって、注文するために作成されたEAの1つを取得し、それを使用して結果を表示しました(EAの名前は示していません)。

4月末にリアル口座でEAを起動することを計画しているとします。取引で利益を得るために最適化する基準を見つける方法は何でしょうか。

3か月のフォワード最適化を開始しましょう。  



最適化後にターミナルを再起動します。

スクリプトを実行し、設定で最良の結果のみを選択します。これがフォルダ内の結果です。


次に、これらすべてのパスの中で最も良いフォワードを視覚的に選択します。同様の結果がいくつかあったので、profit_equity_R_r2を選択しました。これは、この最適化ではドローダウンの低下が優先されるためです。


ストラテジーテスターの同じ期間は次のようになります。


比較のための最大バランスは次のとおりです。


Complex Criterion maxは次のとおりです。


ご覧のとおり、profit_equity_R_r2パスが最高の場合、最大バランスと最大complexの場合よりもチャート上の取引がはるかに少なくなり、利益はほぼ同じですが、チャートははるかになめらかになります。


そこで、カスタム基準のprofit_equity_R_r2を決定しました。ここで、過去3か月間最適化を実行して最適化中に最適な設定を受け取った後、5月にこの設定で取引することにした場合にどうなるかを見てみましょう。

フォワード最適化を実行して確認しましょう。

最適化設定。  


EA設定で、最適化を実行するカスタム基準を設定します。

したがって、profit_equity_R_r2カスタム基準を使用して過去3か月のEAを最適化してから、

4月1日から5月1日までに取得した設定で取引すると、300ユニットのエクイティドローダウンで750ユニットを獲得できます。



それでは検証EAのパフォーマンスをfxsaberで確認しましょう。

EAが4か月間どのように取引されるかを確認します。設定(3ヶ月の最適化と1ヶ月の取引)を検証します。


ご覧のとおり、EAはこのストレステストを生き延びました。

同じ設定であるが、Complex Criterion maxに最適化されているチャートと比較してみましょう。



EAは生き残りましたが...


終わりに

メリット

  1. 最適化結果のすべてのグラフを同時に表示できます。
  2. EAに最適なカスタム最適化基準を見つける可能性。

デメリット

記録されたデータの制限により、グラフの情報量はストラテジーテスターのグラフより少なくなります。

取引数が多いと、フレームを含むファイルが非常に大きくなり、読み取り不能になります。

//---

実験が示しているように、単一の「超基準」はありません。最良の結果は、異なるエキスパートアドバイザーで異なる基準によってもたらされます。

しかし、私たちにはそのような基準の完全なセットがあります。読者がその考えを支持するならば、選択肢はさらに広くなるでしょう。

//---

テスターの1人が記事でスクリプトの設定を説明することを提案したので、資料を勉強したくないユーザは、詳細を勉強せずにコードを使用することができます。


使い方

このコードを使用するには、以下に添付されている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.変化とトレンドを伴うEURUSD クオート(定数)。[コンパイル]を押します。 

変数名の一致を通知するエラーが生成された場合は、名前を変更する必要があります。


設定

コードを挿入すると、EA設定に数行が追加されます。

これらの設定を最適化するためにチェックをオンにしないでください。これらの設定は最適化の結果に影響を与えないため、最適化しません。


  • 取引数が多い場合はパスを書き込みます。EAが多くの取引を実行する場合、このパラメータを増やして、フレームファイルに書き込まれるデータの量を減らすことができます。
  • 利益が%を超える場合はパスを書き込みます。損失を出すパスはデフォルトで削除されます。開始バランスの特定の割合未満の利益を見たくない場合は、これを変更できます。
  • エクイティ値を選択します。カスタム基準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を正しく計算する必要がある場合は、「最小エクイティのみを保存」を設定します。ストラテジーテスターのグラフと同様のグラフが必要な場合は、[最小および最大のエクイティを保存]を選択します。

  • カスタム基準—「なし」に設定すると、フレームはファイルに記録されますが、カスタム基準は計算されません(最適化時間を増やすためではありません)。

ただし、この場合、カスタム基準による最適化を選択することはできません。また、以下のすべてのパラメータは効果がありません。

カスタム基準による最適化を実行する場合は、このパラメータでカスタム基準を選択する必要があります。 

カスタム基準の計算が「エクイティ価値の選択」および「基準の計算」というパラメータに依存することを忘れないでください。

  • 基準を計算 - バランスまたはエクイティに基づいてR^2を計算します。R^2が使用されるすべてのカスタム基準に影響します.。
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R

    //----

    スクリプト設定


    • 回帰直線を描く: どの回帰直線を描くかを選択します(バランス、エクイティ、バランス、エクイティ、なし)。
    • 利益率が次より高い - スクリーンショットの印刷には時間がかかるため、利益がこのパラメータを超えるスクリーンショットのみを印刷するように選択できます。
    • 最良の結果のみ - trueの場合、各カスタム基準の最良の結果を含むスクリーンショットのみを保存します。 falseの場合、すべてを保存します。
    • カスタム基準 - すべてのスクリーンショットが選択されている場合、このパラメータを使用して、フォルダ内のスクリーンショットを並べ替えるカスタム基準を設定できます。
    • 比率 - カスタム基準profit_R_equity_r2、profit_equity_R_r2、profit_equity_r2_Rを計算するための重み。
    • 基準を計算 - バランスまたはエクイティに基づいてR^2を計算します。R^2が使用されるすべてのカスタム基準に影響します.。

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

    • グラフ - テスターの「バックと別々のフォワード」のようにグラフから選択します。フォワードは開始バランスから始まります。  

    または「バックにフォワードが続く」- フォワードはバックワードパスの最後のバランス値から始まります。

    //---

    このWebサイトに掲載された記事は、プログラムの作成に非常に役立ちました。

    ここに記載されている記事のすべての著者に感謝します。


    MetaQuotes Ltdによってロシア語から翻訳されました。
    元の記事: https://www.mql5.com/ru/articles/9922

    添付されたファイル |
    SkrShotOpt.mqh (17.56 KB)
    MQL5.zip (11.59 KB)
    DoEasyライブラリのグラフィックス(第93部): 複合グラフィカルオブジェクトを作成するための機能の準備 DoEasyライブラリのグラフィックス(第93部): 複合グラフィカルオブジェクトを作成するための機能の準備
    本稿では、複合グラフィカルオブジェクトを作成するための機能の開発を始めます。 ライブラリが複合グラフィカルオブジェクトの作成をサポートし、それらのオブジェクトが任意の接続階層を持つことができるようになります。このようなオブジェクトの後続の実装に必要なすべてのクラスを準備します。
    DoEasyライブラリのグラフィックス(第92部): 標準グラフィカルオブジェクトのメモリクラスオブジェクトプロパティの変更履歴 DoEasyライブラリのグラフィックス(第92部): 標準グラフィカルオブジェクトのメモリクラスオブジェクトプロパティの変更履歴
    本稿では標準のグラフィカルオブジェクトメモリのクラスを作成して、プロパティが変更されたときにオブジェクトの状態を保存できるようにします。これにより、以前のグラフィカルオブジェクトの状態に戻ることができるようになります。
    取引における数学:シャープレシオとソルティノレシオ 取引における数学:シャープレシオとソルティノレシオ
    投資収益率は、投資家や初心者のトレーダーが取引効率の分析に使用する最も明白な指標です。プロのトレーダーは、シャープレシオやソルティノレシオなどのより信頼性の高いツールを使用して、ストラテジーを分析します。
    一からの取引エキスパートアドバイザーの開発 一からの取引エキスパートアドバイザーの開発
    この記事では、最小限のプログラミングで自動売買ロボットを開発する方法について説明します。