MetaTrader 5でのモンテカルロ並べ替え検定
はじめに
Aleksey Nikolayevさんが、「モンテカルロ法を適用したトレーディング戦略の最適化」という興味深い記事を書いています。並べ替え検定の方法について説明したもので、検定で得られた一連の取引を無作為に交換するものです。著者は別のタイプの並べ替えテストについて簡単に触れています。そこでは、価格データのシーケンスがランダムに変更され、単一のエクスパートアドバイザー(EA)のパフォーマンスが、同じ価格系列の他の多数のシーケンスのバリエーションでテストされたときに達成されたパフォーマンスと比較されます。
私の意見では、著者はMetaTrader 5を使用して任意のEAでそのようなテストを実施する方法がないことを誤って示唆しています。少なくとも完全にはです。したがって、この記事では、MetaTrader 5を使用して、ランダムに並べ替えられた価格データを含む並べ替え検定について示します。価格系列を並べ替えるコードと、完全なEAの並べ替えテストを実施する準備の初期段階を自動化するスクリプトを紹介します。
並べ替え検定の概要
簡潔に言えば、これから説明する並べ替え検定は、価格データのサンプルを選択するものです。実施されるテストは、サンプルからであることが望ましいです。この価格系列でテストを実行した後、測定に興味がありそうなパフォーマンス基準をメモしておきます。次に、元の価格系列の順序をランダムに変更し、EAをテストしてパフォーマンスに注目します。
これを何度も繰り返し、その都度、価格系列を順列化し、他のテストで指摘したパフォーマンス基準を記録します。これを少なくとも100回、理想的には数千回行う必要があります。並べ替えとテストの回数が多ければ多いほど、結果はより確かなものになります。ただし、ちょっと待ってください。この結果から、テスト対象のEAについて何がわかるというのでしょうか。
並べ替え検定の実施価値
多くの反復テストが実施された場合、最終的には各順列によるパフォーマンス数値のコレクションが出来上がります。シャープレシオでも、プロフィットファクターでも、あるいは単に収支や純利益でもです。99回の並べ替えが行われたとします。元の並べ替えられていないテストを含めば100です。100の数字のパフォーマンスを比較できます。
次のステップは、並べ替えられていないテストのパフォーマンス数値を超えた回数を列挙し、この数値を実施したテストの割合(この例では100)として示すことです。この割合は、EAに利益の可能性がまったくなかった場合に、並び替えられていないテスト以上の結果が偶然に得られる確率です。統計学では、これはと呼ばれ、仮説検定の結果です。
100回反復する仮説的並び替えテストについて続けると、29回の並べ替えテストが、ベンチマークである並べ替えなしのテストよりも優れていました。p値は0.3 (= 29+1/100)となります。つまり、お金を失うEAが、並び替えされていないテスト運用から観察された以上のパフォーマンスを得た確率が0.3であることを意味します。このような結果は有望に思えるかもしれませんが、私たちが望むのは、0.05以下の範囲で限りなくゼロに近いp値です。
完全な計算式を以下に示します。
z+1/r+1
ここで、rは行われた並べ替えの数で、zはより良いパフォーマンスを持つ並べ替えテストの総数です。テストを適切に行うには、並べ替えの手順が重要です。
価格系列の順列化
データの集まりを正しく並べ替えるには、すべての可能な配列の変異が等しく起こりうることを確認しなければなりません。そのためには、0から1の間で一様に分布する乱数を生成する必要があります。MQL5標準ライブラリの統計ライブラリには、このニーズを満たすツールがあります。これを使えば、要求される値の範囲を指定することができます。
//+------------------------------------------------------------------+ //| Random variate from the Uniform distribution | //+------------------------------------------------------------------+ //| Computes the random variable from the Uniform distribution | //| with parameters a and b. | //| | //| Arguments: | //| a : Lower endpoint (minimum) | //| b : Upper endpoint (maximum) | //| error_code : Variable for error code | //| | //| Return value: | //| The random value with uniform distribution. | //+------------------------------------------------------------------+ double MathRandomUniform(const double a,const double b,int &error_code) { //--- check NaN if(!MathIsValidNumber(a) || !MathIsValidNumber(b)) { error_code=ERR_ARGUMENTS_NAN; return QNaN; } //--- check upper bound if(b<a) { error_code=ERR_ARGUMENTS_INVALID; return QNaN; } error_code=ERR_OK; //--- check ranges if(a==b) return a; //--- return a+MathRandomNonZero()*(b-a); }
価格データのシャッフルには独特の要求があります。まず、単純に価格値の位置を変えることはできません。金融時系列に特徴的な時間的関係を乱すことになるからです。そこで、実際の価格の代わりに、価格の変化を順列化します。差分を取る前にまず価格を対数変換することで、生の価格差の変動の影響を最小限に抑えます。
この方法を使うには、最初の価格値を保留し、並べ替えから除外しなければなりません。系列を再構築すると、元の価格系列に存在したトレンドが維持されることになります。唯一のバリエーションは、元の系列の同じ初値と終値の間の内部的な値動きです。
実際に価格系列を並べ替える前に、使用するデータを決めなければなりません。MetaTrader 5では、チャートデータはティックデータから構成されるバーとして表示されます。単一の価格系列の順列付けは、バー情報の順列付けよりもはるかに簡単です。そこで、ティックデータを使用します。ティックには生価格以外の情報も含まれるため、ティックを使用すると、他にも多くの複雑な問題が発生します。出来高、時間、ティックフラグの情報があります。
まず、時間とティックフラグの情報は変更されないので、並べ替えルーチンがこの情報を変更することはありません。私たちに関心があるのは、ビッド、アスク、出来高のみです。2つ目の複雑さは、これらの値のどれかがゼロになる可能性から来るもので、対数変換を適用する際に問題を引き起こします。これらの課題を克服する方法を示すために、いくらかコードを見てみましょう。
ティック順列アルゴリズムの実装
インクルードファイルPermuteTicks.mqhに含まれるCPermuteTicksクラスは、ティックの並べ替え手順を実装します。PermuteTicks.mqhの内部では、標準ライブラリのUniform.mqhをインクルードし、設定された範囲内で一様に生成された乱数を出力するユーティリティにアクセスできるようにしています。後続の定義ではこの範囲を指定します。これらの値を変更する必要がある場合は注意して、最小値が実際に最大しきい値よりも小さいことを確認してください。
//+------------------------------------------------------------------+ //| PermuteTicks.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include<Math\Stat\Uniform.mqh> //+-----------------------------------------------------------------------------------+ //| defines: representing range of random values from random number generator | //+-----------------------------------------------------------------------------------+ #define MIN_THRESHOLD 1e-5 #define MAX_THRESHOLD 1.0
CMqlTick構造体は、このクラスが操作するMqlTick構造体の対応するメンバーを表します。その他のティック情報には触れません。
//+------------------------------------------------------------------+ //| struct to handle tick data to be worked on | //+------------------------------------------------------------------+ struct CMqlTick { double ask_d; double bid_d; double vol_d; double volreal_d; };
CPermuteTicksクラスには、3つのprivate配列プロパティがあります。1つ目はm_ticksに保持される元のティック、2つ目はm_logticksに保持される対数変換されたティック、最後はm_differencedに収集される差分化されたティックです。
//+------------------------------------------------------------------+ //| Class to enable permutation of a collection of ticks in an array | //+------------------------------------------------------------------+ class CPermuteTicks { private : MqlTick m_ticks[]; //original tick data to be shuffled CMqlTick m_logticks[]; //log transformed tick data of original ticks CMqlTick m_differenced[]; //log difference of tick data bool m_initialized; //flag representing proper preparation of a dataset //helper methods bool LogTransformTicks(void); bool ExpTransformTicks(MqlTick &out_ticks[]); public : //constructor CPermuteTicks(void); //desctrucotr ~CPermuteTicks(void); bool Initialize(MqlTick &in_ticks[]); bool Permute(MqlTick &out_ticks[]); };
m_initializedは、並べ替えを行う前の前処理が成功したことを示すブール値のフラグです。
このクラスを使用するには、オブジェクトのインスタンスを作成した後、Initialize()メソッドを呼び出す必要があります。このメソッドは、並べ替えの対象となるティックの配列を必要とします。メソッド内部では、アクセスできないクラスの配列のサイズが変更され、LogTranformTicks()がティックデータを変換するために使用されます。ゼロやマイナスの値を避け、1.0に置き換えます。並べ替えが行われ、ログ変換されたティックデータは、ExpTransformTicks() privateメソッドによって元のドメインに戻されます。
//+--------------------------------------------------------------------+ //|Initialize the permutation process by supplying ticks to be permuted| //+--------------------------------------------------------------------+ bool CPermuteTicks::Initialize(MqlTick &in_ticks[]) { //---set or reset initialization flag m_initialized=false; //---check arraysize if(in_ticks.Size()<5) { Print("Insufficient amount of data supplied "); return false; } //---copy ticks to local array if(ArrayCopy(m_ticks,in_ticks)!=int(in_ticks.Size())) { Print("Error copying ticks ", GetLastError()); return false; } //---ensure the size of m_differenced array if(m_differenced.Size()!=m_ticks.Size()-1) ArrayResize(m_differenced,m_ticks.Size()-1); //---apply log transformation to relevant tick data members if(!LogTransformTicks()) { Print("Log transformation failed ", GetLastError()); return false; } //---fill m_differenced with differenced values, excluding the first tick for(uint i=1; i<m_logticks.Size(); i++) { m_differenced[i-1].bid_d=(m_logticks[i].bid_d)-(m_logticks[i-1].bid_d); m_differenced[i-1].ask_d=(m_logticks[i].ask_d)-(m_logticks[i-1].ask_d); m_differenced[i-1].vol_d=(m_logticks[i].vol_d)-(m_logticks[i-1].vol_d); m_differenced[i-1].volreal_d=(m_logticks[i].volreal_d)-(m_logticks[i-1].volreal_d); } //---set the initilization flag m_initialized=true; //--- return true; }
並べ替えられたティックを出力するには、Permute()という適切な名前のメソッドを呼び出します。動的なMqlTick配列という1つのパラメータが必要で、そこに並べ替えられたティックが配置されます。これは、各繰り返しで生成される乱数に応じて、差分ティック値の位置を入れ替えるwhileループの中にあります。
//+------------------------------------------------------------------+ //|Public method which applies permutation and gets permuted ticks | //+------------------------------------------------------------------+ bool CPermuteTicks::Permute(MqlTick &out_ticks[]) { //---zero out tick array ZeroMemory(out_ticks); //---ensure required data already supplied through initialization if(!m_initialized) { Print("not initialized"); return false; } //---resize output array if necessary if(out_ticks.Size()!=m_ticks.Size()) ArrayResize(out_ticks,m_ticks.Size()); //--- int i,j; CMqlTick tempvalue; i=(int)m_ticks.Size()-1; int error_value; double unif_rando; ulong time = GetTickCount64(); while(i>1) { error_value=0; unif_rando=MathRandomUniform(MIN_THRESHOLD,MAX_THRESHOLD,error_value); if(!MathIsValidNumber(unif_rando)) { Print("Invalid random value ",error_value); return(false); } j=(int)(unif_rando*i); if(j>=i) j=i-1; --i; //---swap tick data randomly tempvalue.bid_d=m_differenced[i].bid_d; tempvalue.ask_d=m_differenced[i].ask_d; tempvalue.vol_d=m_differenced[i].vol_d; tempvalue.volreal_d=m_differenced[i].volreal_d; m_differenced[i].bid_d=m_differenced[j].bid_d; m_differenced[i].ask_d=m_differenced[j].ask_d; m_differenced[i].vol_d=m_differenced[j].vol_d; m_differenced[i].volreal_d=m_differenced[j].volreal_d; m_differenced[j].bid_d=tempvalue.bid_d; m_differenced[j].ask_d=tempvalue.ask_d; m_differenced[j].vol_d=tempvalue.vol_d; m_differenced[j].volreal_d=tempvalue.volreal_d; } //---undo differencing for(uint k = 1; k<m_ticks.Size(); k++) { m_logticks[k].bid_d=m_logticks[k-1].bid_d + m_differenced[k-1].bid_d; m_logticks[k].ask_d=m_logticks[k-1].ask_d + m_differenced[k-1].ask_d; m_logticks[k].vol_d=m_logticks[k-1].vol_d + m_differenced[k-1].vol_d; m_logticks[k].volreal_d=m_logticks[k-1].volreal_d + m_differenced[k-1].volreal_d; } //---copy the first tick out_ticks[0].bid=m_ticks[0].bid; out_ticks[0].ask=m_ticks[0].ask; out_ticks[0].volume=m_ticks[0].volume; out_ticks[0].volume_real=m_ticks[0].volume_real; out_ticks[0].flags=m_ticks[0].flags; out_ticks[0].last=m_ticks[0].last; out_ticks[0].time=m_ticks[0].time; out_ticks[0].time_msc=m_ticks[0].time_msc; //---return transformed data return ExpTransformTicks(out_ticks); } //+------------------------------------------------------------------+
すべての反復が完了すると、m_logticks配列は、並べ替えられたm_differenced tickデータを使用して差分を元に戻し、再構築されます。最後に、Permute()メソッドの唯一の引数は、元のティック系列からコピーされた時刻とティックフラグ情報を持つ、元のドメインに戻されたm_logtickデータで満たされます。
//+-------------------------------------------------------------------+ //|Helper method applying log transformation | //+-------------------------------------------------------------------+ bool CPermuteTicks::LogTransformTicks(void) { //---resize m_logticks if necessary if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); //---log transform only relevant data members, avoid negative and zero values for(uint i=0; i<m_ticks.Size(); i++) { m_logticks[i].bid_d=(m_ticks[i].bid>0)?MathLog(m_ticks[i].bid):MathLog(1e0); m_logticks[i].ask_d=(m_ticks[i].ask>0)?MathLog(m_ticks[i].ask):MathLog(1e0); m_logticks[i].vol_d=(m_ticks[i].volume>0)?MathLog(m_ticks[i].volume):MathLog(1e0); m_logticks[i].volreal_d=(m_ticks[i].volume_real>0)?MathLog(m_ticks[i].volume_real):MathLog(1e0); } //--- return true; } //+-----------------------------------------------------------------------+ //|Helper method undoes log transformation before outputting permuted tick| //+-----------------------------------------------------------------------+ bool CPermuteTicks::ExpTransformTicks(MqlTick &out_ticks[]) { //---apply exponential transform to data and copy original tick data member info //---not involved in permutation operations for(uint k = 1; k<m_ticks.Size(); k++) { out_ticks[k].bid=(m_logticks[k].bid_d)?MathExp(m_logticks[k].bid_d):0; out_ticks[k].ask=(m_logticks[k].ask_d)?MathExp(m_logticks[k].ask_d):0; out_ticks[k].volume=(m_logticks[k].vol_d)?(ulong)MathExp(m_logticks[k].vol_d):0; out_ticks[k].volume_real=(m_logticks[k].volreal_d)?MathExp(m_logticks[k].volreal_d):0; out_ticks[k].flags=m_ticks[k].flags; out_ticks[k].last=m_ticks[k].last; out_ticks[k].time=m_ticks[k].time; out_ticks[k].time_msc=m_ticks[k].time_msc; } //--- return true; }
これで価格系列の順列を処理するアルゴリズムができましたが、これはいわば戦いの半分に過ぎません。
並べ替え検定の手順
順列検定の手順は、MetaTrader 5ターミナルの2つの機能を活用します。1つ目は、カスタム銘柄を作成し、そのプロパティを指定する機能です。2つ目は、気配値表示リストの有効な銘柄に従ってEAを最適化する機能です。このプロセスには少なくともあと2つの手順があります。
ティックの並べ替えやカスタム銘柄の作成が可能で、これらを組み合わせることで、既存の銘柄をベースにカスタム銘柄を生成することができます。各カスタム銘柄は、ベースとして使用される銘柄のティックのユニークな順列で指定されます。銘柄の作成は手作業で行うこともできますが、銘柄の作成とパー並び替えされたティックの追加をすべて自動化できるので、手作業の理由はありません。
PrepareSymbolsForPermutationTestsスクリプトはまさにこれを行います。ユーザー指定の入力により、ベース銘柄、順列で使用するベース銘柄からのティックの日付範囲、作成されるカスタム銘柄の数に対応する必要な順列数、新しいカスタム銘柄の名前に付加されるオプションの文字列識別子を設定することができます。//+------------------------------------------------------------------+ //| PrepareSymbolsForPermutationTests.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include<GenerateSymbols.mqh> #property script_show_inputs //--- input parameters input string BaseSymbol="EURUSD"; input datetime StartDate=D'2023.06.01 00:00'; input datetime EndDate=D'2023.08.01 00:00'; input uint Permutations=100; input string CustomID="";//SymID to be added to symbol permutation names //--- CGenerateSymbols generateSymbols(); //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- if(!generateSymbols.Initiate(BaseSymbol,CustomID,StartDate,EndDate)) return; //--- Print("Number of newly generated symbols is ", generateSymbols.Generate(Permutations)); //--- } //+------------------------------------------------------------------+
このスクリプトは、基本銘柄名を使用し、最後に列挙を加えた銘柄名を自動的に作成します。これらすべてを行うコードは、CGenerateSymbolsクラスの定義を含むGenerateSymbols.mqhに含まれています。このクラス定義は、他に2つの依存関係に依存しています。NewSymbol.mqhは、「MQL5 Cookbook:カスタム銘柄を使用した取引戦略のストレステスト」のコードから引用したCNewSymbolクラスの定義を含みます。
//+------------------------------------------------------------------+ //| Class CNewSymbol. | //| Purpose: Base class for a custom symbol. | //+------------------------------------------------------------------+ class CNewSymbol : public CObject { //--- === Data members === --- private: string m_name; string m_path; MqlTick m_tick; ulong m_from_msc; ulong m_to_msc; uint m_batch_size; bool m_is_selected; //--- === Methods === --- public: //--- constructor/destructor void CNewSymbol(void); void ~CNewSymbol(void) {}; //--- create/delete int Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false); bool Delete(void); //--- methods of access to protected data string Name(void) const { return(m_name); } bool RefreshRates(void); //--- fast access methods to the integer symbol properties bool Select(void) const; bool Select(const bool select); //--- service methods bool Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0); bool LoadTicks(const string _src_file_name); //--- API bool SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const; bool SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const; bool SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const; double GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const; long GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const; string GetProperty(ENUM_SYMBOL_INFO_STRING _property) const; bool SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); bool SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); int RatesDelete(const datetime _from,const datetime _to); int RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]); int RatesUpdate(const MqlRates &_rates[]) const; int TicksAdd(const MqlTick &_ticks[]) const; int TicksDelete(const long _from_msc,long _to_msc) const; int TicksReplace(const MqlTick &_ticks[]) const; //--- private: template<typename PT> bool CloneProperty(const string _origin_symbol,const PT _prop_type) const; int CloneTicks(const MqlTick &_ticks[]) const; int CloneTicks(const string _origin_symbol) const; };
このクラスは、既存の銘柄を基に新しいカスタム銘柄を作成するのに役立ちます。最後に必要な依存関係は、すでに遭遇したPermuteTicks.mqhです。
//+------------------------------------------------------------------+ //| GenerateSymbols.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include<PermuteTicks.mqh> #include<NewSymbol.mqh> //+------------------------------------------------------------------+ //| defines:max number of ticks download attempts and array resize | //+------------------------------------------------------------------+ #define MAX_DOWNLOAD_ATTEMPTS 10 #define RESIZE_RESERVE 100 //+------------------------------------------------------------------+ //|CGenerateSymbols class | //| creates custom symbols from an existing base symbol's tick data | //| symbols represent permutations of base symbol's ticks | //+------------------------------------------------------------------+ class CGenerateSymbols { private: string m_basesymbol; //base symbol string m_symbols_id; //common identifier added to names of new symbols long m_tickrangestart; //beginning date for range of base symbol's ticks long m_tickrangestop; //ending date for range of base symbol's ticks uint m_permutations; //number of permutations and ultimately the number of new symbols to create MqlTick m_baseticks[]; //base symbol's ticks MqlTick m_permutedticks[];//permuted ticks; CNewSymbol *m_csymbols[]; //array of created symbols CPermuteTicks *m_shuffler; //object used to shuffle tick data public: CGenerateSymbols(void); ~CGenerateSymbols(void); bool Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date); uint Generate(const uint permutations); };
CGenerateSymbolsには、知っておくべき2つのメンバー関数があります。Initiate()メソッドは、オブジェクトの生成後最初に呼び出される必要があり、すでに述べたスクリプトのユーザー入力に対応する4つのパラメータを持っています。
//+-----------------------------------------------------------------------------------------+ //|set and check parameters for symbol creation, download ticks and initialize tick shuffler| //+-----------------------------------------------------------------------------------------+ bool CGenerateSymbols::Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date) { //---reset number of permutations previously done m_permutations=0; //---set base symbol m_basesymbol=base_symbol; //---make sure base symbol is selected, ie, visible in WatchList if(!SymbolSelect(m_basesymbol,true)) { Print("Failed to select ", m_basesymbol," error ", GetLastError()); return false; } //---set symbols id m_symbols_id=symbols_id; //---check, set ticks date range if(start_date>=stop_date) { Print("Invalid date range "); return false; } else { m_tickrangestart=long(start_date)*1000; m_tickrangestop=long(stop_date)*1000; } //---check shuffler object if(CheckPointer(m_shuffler)==POINTER_INVALID) { Print("CPermuteTicks object creation failed"); return false; } //---download ticks Comment("Downloading ticks"); uint attempts=0; int downloaded=-1; while(attempts<MAX_DOWNLOAD_ATTEMPTS) { downloaded=CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,m_tickrangestart,m_tickrangestop); if(downloaded<=0) { Sleep(500); ++attempts; } else break; } //---check download result if(downloaded<=0) { Print("Failed to get tick data for ",m_basesymbol," error ", GetLastError()); return false; } Comment("Ticks downloaded"); //---return shuffler initialization result return m_shuffler.Initialize(m_baseticks); }
Generate()メソッドは必要な順列数を入力として受け取り、ターミナルの気配値表示に追加された新しいカスタム銘柄の数を返します。
スクリプトの実行結果は、ターミナルの[エキスパート]タブに表示されます。
//+------------------------------------------------------------------+ //| generate symbols return newly created or refreshed symbols | //+------------------------------------------------------------------+ uint CGenerateSymbols::Generate(const uint permutations) { //---check permutations if(!permutations) { Print("Invalid parameter value for Permutations "); return 0; } //---resize m_csymbols if(m_csymbols.Size()!=m_permutations+permutations) ArrayResize(m_csymbols,m_permutations+permutations,RESIZE_RESERVE); //--- string symspath=m_basesymbol+m_symbols_id+"_PermutedTicks"; int exists; //---do more permutations for(uint i=m_permutations; i<m_csymbols.Size(); i++) { if(CheckPointer(m_csymbols[i])==POINTER_INVALID) m_csymbols[i]=new CNewSymbol(); exists=m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+string(i+1),symspath,m_basesymbol); if(exists>0) { Comment("new symbol created "+m_basesymbol+m_symbols_id+"_"+string(i+1) ); if(!m_csymbols[i].Clone(m_basesymbol) || !m_shuffler.Permute(m_permutedticks)) break; else { m_csymbols[i].Select(true); Comment("adding permuted ticks"); if(m_csymbols[i].TicksAdd(m_permutedticks)>0) m_permutations++; } } else { Comment("symbol exists "+m_basesymbol+m_symbols_id+"_"+string(i+1) ); m_csymbols[i].Select(true); if(!m_shuffler.Permute(m_permutedticks)) break; Comment("replacing ticks "); if(m_csymbols[i].TicksReplace(m_permutedticks)>0) m_permutations++; else break; } } //---return successful number of permutated symbols Comment(""); //--- return m_permutations; }
次の手順は、ストラテジーテスターで最適化を実行し、最後の最適化方法を選択し、テストするEAを指定することです。テストには長い時間がかかる可能性があるため、テストを開始してしばらくの間は、他にやるべきことを見つけます。ストラテジーテスターが終われば、パフォーマンスデータのコレクションが手に入ります。
例
同梱のMACDサンプルEAを使ってテストを実行することで、実行の様子を見てみましょう。テストは、スクリプトで設定された100の順列でAUDUSD銘柄で実行されます。
スクリプトを実行した後、選択したAUDUSD銘柄からのサンプルの順列化されたティックに基づいて、100の余分な銘柄を取得します。
最後に最適化テストを実行します。
使用したEAの設定を以下に示します。
テストの結果
ストラテジーテスターの[結果]タブには、私たちが興味を持ちそうなパフォーマンス数値がすべて表示され、テスターウィンドウの右上隅にあるドロップダウンメニューで選択できる、選択されたパフォーマンス基準に基づいて、銘柄が降順に並べられます。このビューから、p値を手動で簡単に計算することができます。また、必要であれば、テスターから右クリックでエクスポートできる.xmlファイルを処理することで自動的に計算することもできます。
この例を使用すると、計算を実行する必要さえありません。元の銘柄のテスト数値が[結果]タブのずっと下にあり、10を超える並べ替えられた銘柄がより優れたパフォーマンスをクロックしていることがわかるからです。これはp値が0.05以上であることを示しています。
もちろん、テスト期間が非常に短かったため、この結果は多少割り引いて見るべきでしょう。実際の取引で遭遇する可能性の高い条件を代表するような、より実質的な長さのテスト期間を選択する必要があります。
すでに述べたように、p値を計算するには、結果をさらに処理するための多くのオプションが用意されています。それ以降の操作は、ストラテジーテスターからエクスポートされたxmlファイルからのデータ解析が中心となります。ここでは、スプレッドシートアプリケーションを使って、数回のクリックとキー操作でファイルを処理する方法を紹介します。
明らかに、ファイルをエクスポートした後、保存場所をメモし、任意のスプレッドシートアプリを使って開きます。下の図は無料のOpenOffice Calcを使用したもので、表の一番下に新しい行が追加されています。これ以上先に進む前に、計算の一部とすべきでない記号の行を削除したほうがいいでしょう。該当する各列の下で、カスタムマクロを使ってp値が計算されます。マクロの計算式は、各列の並べ替え銘柄のパフォーマンスメトリクスと同様に、並べ替え銘柄のパフォーマンスメトリクス(示されたドキュメントの18行目)を参照します。 マクロの完全な計算式を図に示します。
表計算アプリケーションを使う以外に、xmlファイルを解析するモジュールが豊富にあるpythonを使うこともできます。MQL5に習熟していれば、簡単なスクリプトでファイルを解析することも可能です。テスターから最適化の結果をエクスポートする際には、アクセス可能なディレクトリを選ぶことを忘れないでください。
結論
ソースコードにアクセスすることなく、並べ替えテストをあらゆるEAに適用できることを実証しました。このような並べ替え検定は、関係するデータの分布について仮定を置く必要のない、かなりロバストな統計量を適用するので、非常に貴重です。ストラテジーの開発に用いられる他の多くの統計的検定とは異なります。
最大の欠点は、テストを実施するのに必要な時間と計算資源に関するものです。強力なプロセッサーだけでなく、大容量のストレージも必要になります。新しいティックや銘柄を生成すると、ハードディスクの空き容量が消費されます。私の意見では、EAを購入することがある人は誰でも、この分析方法に注意すべきです。時間はかかりますが、将来的に損をするような間違った決断をせずに済むかもしれません。
順列化された価格データを使用した分析は、複数の方法で適用することができます。 この方法は、ストラテジー開発のさまざまな段階だけでなく、指標の挙動を分析するためにも使うことができます。可能性は広大です。ストラテジーを練ったりテストしたりするとき、データが足りないと思われることがあります。順列化された価格系列を使うことで、テストに利用できるデータが大幅に増えます。この記事で紹介したすべてのMQL5プログラムのソースコードを添付しておきます。
ファイル名 | プログラムの種類 | 詳細 |
---|---|---|
GenerateSymbols.mqh | インクルードファイル | 選択された基本銘柄から並べ替えられたティックデータを持つ銘柄を生成するためのCGenerateSymbolsクラスの定義が含まれている |
NewSymbol.mqh | インクルードファイル | カスタム銘柄を作成するためのCNewSymbolクラス定義が含まれている |
PermuteTicks.mqh | インクルードファイル | ティックデータの配列の順列を作成するためのCPermuteTicksクラスを定義する |
PrepareSymbolsForPermutationTests.mq5 | スクリプトファイル | 並べ替えテストの準備として、並べ替えられたティックを持つカスタム銘柄の作成を自動化するスクリプト |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13162
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索