English Русский 中文 Deutsch Português
preview
信頼区間を用いて将来のパフォーマンスを見積もる

信頼区間を用いて将来のパフォーマンスを見積もる

MetaTrader 5テスター | 22 1月 2024, 10:15
181 0
Francis Dube
Francis Dube

はじめに

収益性の高い自動売買システムを作るのは簡単なことではありません。たとえ収益性の高いエキスパートアドバイザー(EA)を作ることができたとしても、それがリスクに見合うかどうかについては疑問が残ります。自分の戦略が割り当てられた資金をすべて使い果たさないことに満足しているかもしれませんが、これはすぐにライブ取引を有効にする理由にはなりません。最終的には利益が動機であり、もし後になって、戦略は確かに利益を上げているが、リスクを正当化できるほど利益を上げていない、あるいは他の投資機会と比較してリターンが低いことに気づけば、間違いなく非常に後悔することになるでしょう。

そこでこの記事では、サンプル外テストから収集したデータを使って、自動売買システムの将来のパフォーマンスを推定するのに役立つ統計学の分野から借用したテクニックを探ります。


パフォーマンスの程度は?

取引システムの候補をテストするとき、当然のことながら、さまざまなパフォーマンスメトリクスのコレクションに行き着きます。このデータは、直感的にシステムの利益の可能性を示してくれますが、この直感だけでは十分ではないかもしれません。テストでは十分な利益を上げたストラテジーも、実戦で取引すると、あまり良いリターンが得られないこともあります。テスト中に観察されたパフォーマンスが同じレベルで継続するかどうか、より良いアイデアを得る方法はあるのでしょうか。そうでなければ、パフォーマンスはどの程度悪化するのでしょうか。

そこで標準的な統計的手法が役に立ちます。これから説明するテクニックは、正確な推定を意味するものではなく、決して正確な推定にはならないことに留意すべきです。これから説明するテクニックがすることは、大きな、あるいは許容できる利益を生み出す確率の高い戦略を特定する方法を提供することです。

私は、生のシャープレシオの数値を使って、将来のパフォーマンスを確率的に仮定している人たちをたくさん見てきました。これは危険です。過去の実績が将来の利益を示すものではないことを忘れてはいけません。金融市場はバカにできません。価格チャートはあっちへ行ったりこっちへ行ったりしていて、多くの場合原因は不明です。私たちがやりたいのは、意思決定プロセスに適用できる適切な確率に基づくパフォーマンス予測を計算することです。


信頼区間

信頼区間


信頼区間とは、データの集まりまたは母集団のある統計量が、ある時間の割合である範囲内にある確率のことです。それらは、計算された水準が推定された真の統計量を含む確率を計算することによって、確実性の程度を測定します。統計学者は通常、90%から99%の信頼度を用います。これらの間隔は、さまざまな方法で計算することができます。この記事では、一般的なブーストラップのテクニックをいくつか取り上げます。


ブートストラップ

統計学におけるブートストラップ法とは、データの集合を使用して、元のデータから無作為に抽出または選択することによって、他の多数の新しいデータセットを作成する手順のことです。新しいデータセットは元のデータセットと同じメンバーを持ちますが、新しいデータセットのメンバーの一部は重複します。

元の値
ブートストラップ1
ブートストラップ2
ブートストラップ3
 ブートストラップ4
A
A
A
A
B
B
A
B
B
B
C
B
B
B
C
D
C
D
C
D
E
D
E
C
E


上の表がそれをよく表しています。「元の値」列は元のデータセットを表し、他の列は元の値から構築されたデータセットを表します。見てわかるように、ブートストラップされた列には1つ以上の重複があります。これを何度も繰り返すことで、現在は観測できない、あるいは未知のサンプルを表すことができるデータをたくさん生成することができます。ブートストラップを取引に応用した例は、「モンテカルロ法を適用したトレーディング戦略の最適化」稿ですでに見ています。

ブートストラップ理論の中心は、元のデータセットが、観測できずモデル化しようとしている、より大きなデータの集まり(母集団)を代表するものでなければならないということです。したがって、これらのブートストラップを作成するとき、それらは観察不可能なコレクションのプロキシとなります。これらのブートストラップと元のサンプルの統計的特性は、未知で観察不可能な母集団の結論を導き出すために使用することができます。


信頼区間のブートストラップ

信頼区間をブーストラップする3つの方法を示します。すなわち、ピボット法、パーセンタイル法、そして最後にバイアス補正加速法(bcd)です。

ピボット法(英語)では、多数のブートストラップを作成し、それを用いて検定統計量を計算します。検定統計量とは、推定しようとしている母集団の特徴を示すもので、平均値や中央値などです。そして,推定境界は,ブートストラップ標本の期待値をオリジナルまで増加させるために必要なものに対して,オリジナルの日付集合からの検定統計量の値を調整することによって見つけられます。

パーセンタイル法は,ブートストラップされた標本から計算された検定統計量の分布を考慮します。この分布は,未知母集団の分布に類似していると仮定されます。境界は,ブートストラップされた標本から得られる計算された検定統計量の分布のパーセンタイル間の区間となります。 

バイアス補正加速法(英語)は、もう少し洗練されています。ブートストラップを作成し、それぞれの検定統計量を計算しました。バイアス補正係数を計算しますが、これはブートストラップ推定値が元のデータセットの推定値より小さい割合です。そして、ジャックナイフ法 (英語)と呼ばれる方法で加速度係数を算出します。これは、変換された検定統計量の分散がその値に依存する度合いを推定するために使用されるもう1つのリサンプリング手法です。

その後、パーセンタイル法を用いて下限値と上限値を計算し、バイアス補正と加速係数に応じて修正します。最終的な信頼区間は、並べ替え後の修正値から得られます。

これらのテクニックをコードでどのように実装できるか見てみましょう。


CBoostrapクラス

CBoostrapは、今説明した3つのブートストラップ法を用いた信頼区間の計算をカプセル化するクラスです。これにより、ユーザーは複数の設定可能な確率の信頼区間を計算することができ、また生成するブートストラップの数を指定することができます。

#include<Math\Alglib\specialfunctions.mqh>
#include<Math\Stat\Math.mqh>
#include<UniformRandom.mqh>


クラスの定義は、標準ライブラリからいくつかの必須数学ユーティリティを取り入れることから始まります。

//+------------------------------------------------------------------+
//|Function pointer                                                  |
//+------------------------------------------------------------------+
typedef double(*BootStrapFunction)(double &in[],int stop=-1);


BootStrapFunction関数ポインタは,検定統計量または母集団パラメータを計算するための関数シグネチャを定義します。

//+------------------------------------------------------------------+
//|Boot strap types                                                  |
//+------------------------------------------------------------------+
enum ENUM_BOOSTRAP_TYPE
  {
   ENUM_BOOTSTRAP_PIVOT=0,
   ENUM_BOOTSTRAP_PERCENTILE,
   ENUM_BOOTSTRAP_BCA
  };


ENUM_BOOSTRAP_TYPE列挙は、特定のブーストラップ計算方法(ピボット、パーセンタイル、BCA)の選択を容易にします。

//+------------------------------------------------------------------+
//|Constructor                                                       |
//+------------------------------------------------------------------+
CBootstrap::CBootstrap(const ENUM_BOOSTRAP_TYPE boot_type,const uint nboot,const BootStrapFunction function,double &in_samples[])
  {
//--- set the function pointer
   m_function=function;
//--- optimistic initilization of flag
   m_initialized=true;
//--- set method of boostrap to be applied
   m_boot_type=boot_type;
//--- set number of boostrap iterations
   m_replications=nboot;
//---make sure there are at least 5 boostraps
   if(m_replications<5)
      m_initialized=false;
//--- initilize random number generator
   m_unifrand=new CUniFrand();
   if(m_unifrand!=NULL)
      m_unifrand.SetSeed(MathRand());
   else
      m_initialized=false;
//--- copy samples to internal buffer
   if(ArrayCopy(m_data,in_samples)!=ArraySize(in_samples))
     {
      Print("Data Copy error ", GetLastError());
      m_initialized=false;
     }
//--- initialize shuffled buffer
   if(ArrayCopy(m_shuffled,in_samples)!=ArraySize(in_samples))
     {
      Print("Data Copy error ", GetLastError());
      m_initialized=false;
     }
//--- set memory for bootstrap calculations container
   if(ArrayResize(m_rep_cal,(int)m_replications)!=(int)m_replications)
     {
      Print("Memory allocation error ", GetLastError());
      m_initialized=false;
     }
//--- check function pointer
   if(m_function==NULL)
     {
      Print("Invalid function pointer");
      m_initialized=false;
     }
  }

CBoostrapは,パラメトリックコンストラクタによって定義され,その入力パラメータは,ブーストラップ操作の性質を決定します。

  • boot_type:ブーストラップの計算方法を設定する。
  • nboot:生成されるブーストラップサンプルの数を表す。少なくとも100個を持つことが推奨されるが、ロバストな結果を得るためには数千個を生成することがより理想的。
  • function:推定される母集団パラメータを計算するためのユーザー提供の関数定義を指す。この関数のパラメータは、検定統計量を計算するために使用されるデータサンプルの配列です。関数ポインタのデフォルトの整数パラメータは、計算に使用される配列メンバーの数を定義します。
  • 最後にin_samples配列は、ブートストラップが生成されるデータのコンテナです。この同じデータセットとそのブートストラップされたバリエーションが、検定統計量を計算する関数ポインタに渡されます。
//+------------------------------------------------------------------+
//| public method for calculating confidence intervals               |
//+------------------------------------------------------------------+
bool CBootstrap::CalculateConfidenceIntervals(double &in_out_conf[])
  {
//--- safety check
   if(!m_initialized)
     {
      ZeroMemory(in_out_conf);
      return m_initialized;
     }
//--- check input parameter values
   if(ArraySize(in_out_conf)<=0 ||
      in_out_conf[ArrayMaximum(in_out_conf)]>=1 ||
      in_out_conf[ArrayMinimum(in_out_conf)]<=0)
     {
      Print("Invalid input values for function ",__FUNCTION__,"\n All values should be probabilities between 0 and 1");
      return false;
     }
//--- do bootstrap based on chosen method
   switch(m_boot_type)
     {
      case ENUM_BOOTSTRAP_PIVOT:
         return pivot_boot(in_out_conf);
      case ENUM_BOOTSTRAP_PERCENTILE:
         return percentile_boot(in_out_conf);
      case ENUM_BOOTSTRAP_BCA:
         return bca_boot(in_out_conf);
      default:
         return false;
     }
//---
  }


CalculateConfidenceIntervals()クラスに2つだけあるpublicメソッドのうちの1つは、確率値の配列を入力として受け取ります。ユーザーが望む数だけです。これらの値は、真のパラメータ値が計算された区間内にある確率を定義します。

例えば、確率が90%である信頼区間を計算するために、ユーザーは値0.9の配列を与え、メソッドは値のペアを返します。これらの返された値は、入力として与えられたのと同じ配列に書き込まれます。入力配列の各メンバーに対して,このメソッドは値のペアに置き換えます。各ペアの最初の値は間隔の下限で、2番目の値は上限です。

前述のように、異なる確率で複数の信頼区間を要求することは可能です。出力は、入力として指定された最も低い確率から最も高い確率の順に境界を並べます。

クラスの使い方を実演する前に、取引戦略のパフォーマンスを測定するために使用するデータを定義する必要があります。通常、戦略のパフォーマンスをリターンによって分類するのが標準的なやり方です。この値を計算するためには、エクイティカーブとリターンシリーズを調べる必要があります。

ストラテジーのリターンシリーズを使って、さまざまなパフォーマンス指標を計算することができます。物事をシンプルにするために、年率平均リターンを検定統計量として使用し、その将来値を指定した信頼度で推定します。

この検定統計量を使えば、ある戦略に期待できる最低の平均リターンを測ることができます。また、信頼度の上限は、すべてがうまくいった場合にパフォーマンスがどの程度良くなるかの大まかな目安になります。


CReturnsクラス

将来の平均リターンを近似するために必要な一連のリターンを収集するために、CReturnsクラスを使用します。このクラスは、「取引における数学:シャープレシオとソルティーノレシオ」稿で紹介されているコードを引用しています。このバージョンの特徴は、パフォーマンス計算に使用するリターンシリーズのタイプを選択できることです。

//+------------------------------------------------------------------+
//| Class for calculating Sharpe Ratio in the tester                 |
//+------------------------------------------------------------------+
class CReturns
  {
private:
   CArrayDouble*     m_all_bars_equity;
   CArrayDouble*     m_open_position_bars_equity;
   CArrayDouble*     m_trade_equity;

   CArrayDouble*     m_all_bars_returns;
   CArrayDouble*     m_open_position_bars_returns;
   CArrayDouble*     m_trade_returns;

   int               ProcessHistory(void);
   void              CalculateReturns(CArrayDouble &r,CArrayDouble &e);

public:
                     CReturns(void);
                    ~CReturns(void);

   void              OnNewTick(void);

   bool              GetEquityCurve(const ENUM_RETURNS_TYPE return_type,double &out_equity[]);
   bool              GetReturns(const ENUM_RETURNS_TYPE return_type,double &out_returns[]);
  };



returns.mqhは、リターンシリーズのタイプを決定する列挙型を定義しています。ENUM_RETURNS_ALL_BARSは、テスト期間のすべてのバーについて、バーごとの一連のリターンを定義します。ENUM_RETURNS_POSITION_OPEN_BARSは、ポジションが開かれたバーのバーごとのリターンを構成する一連のリターンです。ENUM_RETURNS_TRADESは、完了した取引のリターンシリーズのみを定義します。このオプションでは、バーごとの情報は収集されません。

//+------------------------------------------------------------------+
//| Enumeration specifying granularity of return                     |
//+------------------------------------------------------------------+
enum ENUM_RETURNS_TYPE
  {
   ENUM_RETURNS_ALL_BARS=0,//bar-by-bar returns for all bars
   ENUM_RETURNS_POSITION_OPEN_BARS,//bar-by-bar returns for bars with open trades
   ENUM_RETURNS_TRADES//trade returns
  };

CReturnsクラスを使って、エクイティカーブを定義する一連のエクイティ値をGetEquityCurve()メソッドで取得することができます。

//+------------------------------------------------------------------+
//| get equity curve                                                 |
//+------------------------------------------------------------------+
bool CReturns::GetEquityCurve(const ENUM_RETURNS_TYPE return_type,double &out_equity[])
  {
   int m_counter=0;
   CArrayDouble *equity;
   ZeroMemory(out_equity);
//---
   switch(return_type)
     {
      case ENUM_RETURNS_ALL_BARS:
         m_counter=m_all_bars_equity.Total();
         equity=m_all_bars_equity;
         break;
      case ENUM_RETURNS_POSITION_OPEN_BARS:
         m_counter=m_open_position_bars_equity.Total();
         equity=m_open_position_bars_equity;
         break;
      case ENUM_RETURNS_TRADES:
         m_counter=(m_trade_equity.Total()>1)?m_trade_equity.Total():ProcessHistory();
         equity=m_trade_equity;
         break;
      default:
         return false;
     }
//--- if there are no bars, return 0
   if(m_counter < 2)
      return false;
//---
   if(ArraySize(out_equity)!=m_counter)
      if(ArrayResize(out_equity,equity.Total()) < m_counter)
         return false;
//---
   for(int i=0; i<equity.Total(); i++)
      out_equity[i]=equity[i];
//---
   return(true);
//---
  }

同様に、GetReturns()は一連のリターンを出力できます。どちらのメソッドも、希望する特定のリターン系列と、値を受け取る配列を入力として受け取ります。

//+------------------------------------------------------------------+
//|Gets the returns into array                                       |
//+------------------------------------------------------------------+
bool CReturns::GetReturns(const ENUM_RETURNS_TYPE return_type,double &out_returns[])
  {
//---
   CArrayDouble *returns,*equity;
   ZeroMemory(out_returns);
//---
   switch(return_type)
     {
      case ENUM_RETURNS_ALL_BARS:
         returns=m_all_bars_returns;
         equity=m_all_bars_equity;
         break;
      case ENUM_RETURNS_POSITION_OPEN_BARS:
         returns=m_open_position_bars_returns;
         equity=m_open_position_bars_equity;
         break;
      case ENUM_RETURNS_TRADES:
         if(m_trade_equity.Total()<2)
            ProcessHistory();
         returns=m_trade_returns;
         equity=m_trade_equity;
         break;
      default:
         return false;
     }
//--- if there are no bars, return 0
   if(equity.Total() < 2)
      return false;
//--- calculate average returns
   CalculateReturns(returns,equity);
//--- return the mean return
   if(returns.Total()<=0)
      return false;
//---
   if(ArraySize(out_returns)!=returns.Total())
      if(ArrayResize(out_returns,returns.Total()) < returns.Total())
         return false;
//---
   for(int i=0; i<returns.Total(); i++)
      out_returns[i]=returns[i];
//---
   return(true);
//---
  }



一例

以下のEAのコードは、CReturnsを使って一連のリターンを収集する方法を示しています。この例では、リターンシリーズはバイナリファイルに保存されます。OnTesterの中でCBootstrapを使って信頼区間計算をおこなうことは可能ですが、この例では、代わりに別のプログラムからこのシリーズを分析します。

//+------------------------------------------------------------------+
//|                                           MovingAverage_Demo.mq5 |
//|                        Copyright 2023, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Returns.mqh>
#include <Bootstrap.mqh>
#include <Files\FileBin.mqh>
#include <Trade\Trade.mqh>

input double MaximumRisk        = 0.02;    // Maximum Risk in percentage
input double DecreaseFactor     = 3;       // Descrease factor
input int    MovingPeriod       = 12;      // Moving Average period
input int    MovingShift        = 6;       // Moving Average shift
input ENUM_RETURNS_TYPE rtypes  = ENUM_RETURNS_ALL_BARS; // return types to record
input uint BootStrapIterations  = 10000;
input double BootStrapConfidenceLevel = 0.975;
input ENUM_BOOSTRAP_TYPE AppliedBoostrapMethod=ENUM_BOOTSTRAP_BCA;
input bool   SaveReturnsToFile = true;
input string ReturnsFileName = "MovingAverage_Demo";

//---
int    ExtHandle=0;
bool   ExtHedging=false;
CTrade ExtTrade;
CReturns ma_returns;
#define MA_MAGIC 1234501
//+------------------------------------------------------------------+
//| Calculate optimal lot size                                       |
//+------------------------------------------------------------------+
double TradeSizeOptimized(void)
  {
   double price=0.0;
   double margin=0.0;
//--- select lot size
   if(!SymbolInfoDouble(_Symbol,SYMBOL_ASK,price))
      return(0.0);
   if(!OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,1.0,price,margin))
      return(0.0);
   if(margin<=0.0)
      return(0.0);

   double lot=NormalizeDouble(AccountInfoDouble(ACCOUNT_MARGIN_FREE)*MaximumRisk/margin,2);
//--- calculate number of losses orders without a break
   if(DecreaseFactor>0)
     {
      //--- select history for access
      HistorySelect(0,TimeCurrent());
      //---
      int    orders=HistoryDealsTotal();  // total history deals
      int    losses=0;                    // number of losses orders without a break

      for(int i=orders-1; i>=0; i--)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket==0)
           {
            Print("HistoryDealGetTicket failed, no trade history");
            break;
           }
         //--- check symbol
         if(HistoryDealGetString(ticket,DEAL_SYMBOL)!=_Symbol)
            continue;
         //--- check Expert Magic number
         if(HistoryDealGetInteger(ticket,DEAL_MAGIC)!=MA_MAGIC)
            continue;
         //--- check profit
         double profit=HistoryDealGetDouble(ticket,DEAL_PROFIT);
         if(profit>0.0)
            break;
         if(profit<0.0)
            losses++;
        }
      //---
      if(losses>1)
         lot=NormalizeDouble(lot-lot*losses/DecreaseFactor,1);
     }
//--- normalize and check limits
   double stepvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
   lot=stepvol*NormalizeDouble(lot/stepvol,0);

   double minvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   if(lot<minvol)
      lot=minvol;

   double maxvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
   if(lot>maxvol)
      lot=maxvol;
//--- return trading volume
   return(lot);
  }
//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   MqlRates rt[2];
//--- go trading only for first ticks of new bar
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
   if(rt[1].tick_volume>1)
      return;
//--- get current Moving Average
   double   ma[1];
   if(CopyBuffer(ExtHandle,0,0,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- check signals
   ENUM_ORDER_TYPE signal=WRONG_VALUE;

   if(rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=ORDER_TYPE_SELL;    // sell conditions
   else
     {
      if(rt[0].open<ma[0] && rt[0].close>ma[0])
         signal=ORDER_TYPE_BUY;  // buy conditions
     }
//--- additional checking
   if(signal!=WRONG_VALUE)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         ExtTrade.PositionOpen(_Symbol,signal,TradeSizeOptimized(),
                               SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK),
                               0,0);
     }
//---
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(void)
  {
   MqlRates rt[2];
//--- go trading only for first ticks of new bar
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
   if(rt[1].tick_volume>1)
      return;
//--- get current Moving Average
   double   ma[1];
   if(CopyBuffer(ExtHandle,0,0,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- positions already selected before
   bool signal=false;
   long type=PositionGetInteger(POSITION_TYPE);

   if(type==(long)POSITION_TYPE_BUY && rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=true;
   if(type==(long)POSITION_TYPE_SELL && rt[0].open<ma[0] && rt[0].close>ma[0])
      signal=true;
//--- additional checking
   if(signal)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         ExtTrade.PositionClose(_Symbol,3);
     }
//---
  }
//+------------------------------------------------------------------+
//| Position select depending on netting or hedging                  |
//+------------------------------------------------------------------+
bool SelectPosition()
  {
   bool res=false;
//--- check position in Hedging mode
   if(ExtHedging)
     {
      uint total=PositionsTotal();
      for(uint i=0; i<total; i++)
        {
         string position_symbol=PositionGetSymbol(i);
         if(_Symbol==position_symbol && MA_MAGIC==PositionGetInteger(POSITION_MAGIC))
           {
            res=true;
            break;
           }
        }
     }
//--- check position in Netting mode
   else
     {
      if(!PositionSelect(_Symbol))
         return(false);
      else
         return(PositionGetInteger(POSITION_MAGIC)==MA_MAGIC); //---check Magic number
     }
//--- result for Hedging mode
   return(res);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- prepare trade class to control positions if hedging mode is active
   ExtHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   ExtTrade.SetExpertMagicNumber(MA_MAGIC);
   ExtTrade.SetMarginMode();
   ExtTrade.SetTypeFillingBySymbol(Symbol());
//--- Moving Average indicator
   ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
   if(ExtHandle==INVALID_HANDLE)
     {
      printf("Error creating MA indicator");
      return(INIT_FAILED);
     }
//--- ok
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ma_returns.OnNewTick();
//---
   if(SelectPosition())
      CheckForClose();
   else
      CheckForOpen();
//---
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
   double returns[],confidence[],params[];
   ArrayResize(confidence,1);
   confidence[0]=BootStrapConfidenceLevel;
//---
   double ret=0.0;
//---
   if(ma_returns.GetReturns(rtypes,returns))
     {
      CBootstrap minreturn(AppliedBoostrapMethod,BootStrapIterations,MeanReturns,returns);

      if(minreturn.CalculateConfidenceIntervals(confidence))
        {
         ret=confidence[0];
         string fname=ReturnsFileName+"_"+_Symbol+".returns";
         CFileBin file;
         if(SaveReturnsToFile && file.Open(fname,FILE_WRITE|FILE_COMMON)!=INVALID_HANDLE)
            file.WriteDoubleArray(returns);

        }

     }
//---
   return(ret);
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|the bootstrap function                                            |
//+------------------------------------------------------------------+
double MeanReturns(double &rets[], int upto=-1)
  {
   int stop=(upto<=0)?ArraySize(rets):upto;

   if(!stop)
     {
      Print("in danger of zero divide error ",__FUNCTION__);
      return 0;
     }

   double sum=0;
   for(int i=0; i<stop; i++)
      sum+=rets[i];

   sum/=double(stop);

   switch(Period())
     {
      case PERIOD_D1:
         sum*=252;
         return sum;
      case PERIOD_W1:
         sum*=52;
         return sum;
      case PERIOD_MN1:
         sum*=12;
         return sum;
      default:
         sum*=double(PeriodSeconds(PERIOD_D1) / PeriodSeconds());
         return sum*=252;
     }

  }


スクリプトは保存されたデータを読み、CBootstrapのインスタンスに渡します。検定統計量は,BootStrapFunction関数ポインタのシグネチャと一致するMeanReturns()関数によって計算されます。90%、95%、97.5%の信頼区間に対応する値0.9、0.95、0.975を持つ配列でCalculateConfidenceIntervals()を呼び出します。

//+------------------------------------------------------------------+
//|                                       ApproximateMeanReturns.mq5 |
//|                        Copyright 2023, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<Math\Stat\Math.mqh>
#include<Files\FileBin.mqh>
#include<Bootstrap.mqh>
//--- input parameters
input string   FileName="MovingAverage_Demo_EURUSD.returns";//returns file name
input ENUM_BOOSTRAP_TYPE AppliedBoostrapMethod=ENUM_BOOTSTRAP_BCA;
input uint BootStrapIterations=10000;
input string BootStrapProbability="0.975,0.95,0.90";
//---
CBootstrap *meanreturns;
double logreturns[],bounds[],bootstraps[];
string sbounds[];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   int done=StringSplit(BootStrapProbability,StringGetCharacter(",",0),sbounds);
//---
   if(done)
     {
      ArrayResize(bounds,done);
      for(int i=0; i<done; i++)
         bounds[i]=StringToDouble(sbounds[i]);
      if(ArraySort(bounds))
         for(int i=0; i<done; i++)
            sbounds[i]=DoubleToString(bounds[i]);
     }
//---
   if(!done)
     {
      Print("error parsing inputs ", GetLastError());
      return;
     }
//---
   if(!LoadReturns(FileName,logreturns))
      return;
//---
   meanreturns=new CBootstrap(AppliedBoostrapMethod,BootStrapIterations,MeanReturns,logreturns);
//---
   if(meanreturns.CalculateConfidenceIntervals(bounds))
     {
      for(int i=0; i<done; i++)
         Print(EnumToString(AppliedBoostrapMethod)," ",sbounds[i],": ","(",bounds[i*2]," ",bounds[(i*2)+1],")");
     }
//---
   delete meanreturns;
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Load returns from file                                           |
//+------------------------------------------------------------------+
bool LoadReturns(const string fname,double &out_returns[])
  {
   CFileBin file;
//---
   if(file.Open(fname,FILE_READ|FILE_COMMON)==INVALID_HANDLE)
      return false;
//---
   if(!file.ReadDoubleArray(out_returns))
     {
      Print("File read error ",GetLastError());
      return false;
     }
//---
   return true;
  }
//+------------------------------------------------------------------+
//|the bootstrap function                                            |
//+------------------------------------------------------------------+
double MeanReturns(double &rets[], int upto=-1)
  {
   int stop=(upto<=0)?ArraySize(rets):upto;

   if(!stop)
     {
      Print("in danger of zero divide error ",__FUNCTION__);
      return 0;
     }

   double sum=0;
   for(int i=0; i<stop; i++)
      sum+=rets[i];

   sum/=double(stop);

   switch(Period())
     {
      case PERIOD_D1:
         sum*=252;
         return sum;
      case PERIOD_W1:
         sum*=52;
         return sum;
      case PERIOD_MN1:
         sum*=12;
         return sum;
      default:
         sum*=double(PeriodSeconds(PERIOD_D1) / PeriodSeconds());
         return sum*=252;
     }

  }
//+------------------------------------------------------------------+



計算された区間の最終出力を見る前に、常にブートストラップされた検定統計量の分布プロットを見ることをお勧めします。これは、GetBootStrapStatistics()によってアクセスされたデータをプロットすることによっておこなうことができます。

ブストラップ分布



移動平均EAの結果を見ると、OnTesterがマイナスの数値を返していることがわかります。これは、1回のテストでプラスの結果が表示されたにもかかわらず、将来のパフォーマンスが悪化する可能性があることを示しています。-0.12は予想される最悪の平均リターンです。 

結果


異なる信頼区間での結果を以下に示します。

ApproximateMeanReturns (EURUSD,D1)      ENUM_BOOTSTRAP_BCA 0.90000000: (-0.07040966776550685 0.1134376873958945)
ApproximateMeanReturns (EURUSD,D1)      ENUM_BOOTSTRAP_BCA 0.95000000: (-0.09739322056041048 0.1397669758772337)
ApproximateMeanReturns (EURUSD,D1)      ENUM_BOOTSTRAP_BCA 0.97500000: (-0.12438450770122121 0.1619709975134838)

この例では、移動平均EAの予測確率に基づく平均リターンの計算を示します。他のパフォーマンス指標にも同じ原理を使うことができます。しかし、一言注意が必要です。比率に基づくパフォーマンス指標には問題があります。その理由は、指標の計算における分母にあります。分母が本当に小さくなると、非常に大きな数字になってしまいます。

特定の指標の将来のパフォーマンスを推定するためにこれらの方法を使用することの適合性を判断する最善の方法は、ブーストラップされたサンプル統計の分布を研究することです。注目しているのは、重い尾を引いていないかどうかです。重い尾を持つ分布から得られた結果は、注意して使用すべきです。

同じEAの最悪の場合のシャープ レシオを推定する例を見てみましょう。これは、CBootstrapコンストラクタの関数ポインタパラメータに渡された関数を書き換えることによって達成されます。

このテストの結果は、単独テストの結果よりもはるかに悪いことを示しています。

シャープレシオの推定


結論

将来予想されるパフォーマンスの範囲を知ることは、戦略選択に関してより良い投資判断を下すのに役立ちます。実証された方法は教科書的な静力学に基づいているが、ユーザーは固有の限界に注意する必要があります。

計算された信頼区間は、それが基づいているデータと同じくらい良いものでしかありません。計算に使われたサンプルが不適切であれば、古典的なガベージインガベージアウトのシナリオに陥ってしまいます。常に、将来遭遇する可能性の高い状況を代表する適切なサンプルを選択することが重要です。

ファイル名
詳細
Mql5files\include\Bootstrap.mqh
CBootstrapクラスの定義を含む
Mql5files\include\Returns.mqh
CReturnsクラスの定義を含む
Mql5files\include\UniformRandom.mqh
0と1の間の一様分布数を生成するクラス
Mql5files\scripts\ApproximateMeanReturns.mq5
ストラテジーテスターから保存されたファイルを読み込み、プロジェクトの平均リターンの信頼区間を計算するスクリプト
Mql5files\experts\ MovingAverage_Demo.mq5
CBootstrapとCReturnsの適用を実証するために使用されるEA


MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13426

添付されたファイル |
Bootstrap.mqh (12.73 KB)
Returns.mqh (9.71 KB)
UniformRandom.mqh (2.84 KB)
mql5files.zip (10.97 KB)
GUI:MQLで独自のグラフィックライブラリを作成するためのヒントとコツ GUI:MQLで独自のグラフィックライブラリを作成するためのヒントとコツ
GUIライブラリの基本的な使い方を説明し、GUIライブラリがどのように機能するのかを理解し、さらには自分自身のライブラリを作り始めることができるようにします。
指標やEAのデータを表示するダッシュボードの作成 指標やEAのデータを表示するダッシュボードの作成
この記事では、指標とEAで使用するダッシュボードクラスを作成します。これは、エキスパートアドバイザー(EA)に標準指標を含めて使用するためのテンプレートを含む短い連載の紹介記事です。まず、MetaTrader 5データウィンドウに似たパネルを作成します。
MQL5を使ったシンプルな多通貨エキスパートアドバイザーの作り方(第2回):指標シグナル:多時間枠放物線SAR指標 MQL5を使ったシンプルな多通貨エキスパートアドバイザーの作り方(第2回):指標シグナル:多時間枠放物線SAR指標
この記事の多通貨エキスパートアドバイザー(EA)は、1つの銘柄チャートからのみ複数の銘柄ペアの取引(注文を出す、注文を決済する、トレーリングストップロスとトレーリングプロフィットなどで注文を管理するなど)ができるEAまたは自動売買ロボットです。今回は、PERIOD_M15からPERIOD_D1までの多時間枠でパラボリックSARまたはiSARという1つの指標のみを使用します。
リプレイシステムの開発 - 市場シミュレーション(第20回):FOREX (I) リプレイシステムの開発 - 市場シミュレーション(第20回):FOREX (I)
この記事の最初の目的は、外国為替取引のすべての可能性をカバーすることではなく、少なくとも1つのマーケットリプレイを実行できるようにシステムを適応させることです。シミュレーションはまた別の機会にしますが、ティックがなくバーだけでも、少しの努力で外国為替市場で起こりうる取引をシミュレートすることができます。シミュレーターをどのように適応させるかを検討するまでは、この状態が続くでしょう。システム内部でFXのデータに手を加えずに作業しようとすると、さまざまなエラーが発生します。