
MQL5における組合せ対称交差検証法
はじめに
自動化されたストラテジーを作るとき、それは何らかの方法で改良する必要がある、任意の指標に基づくルールのアウトラインから始めることがあります。この精緻化のプロセスでは、選択した指標のパラメータ値を変えて複数のテストを実施します。そうすることで、利益やその他の指標を最大化する指標値を見つけることができます。この方法の問題点は、金融時系列にノイズが多く、ある程度の楽観的バイアスが生じることです。過剰学習と呼ばれる現象です。
過剰学習は避けられませんが、その程度は戦略によって異なります。したがって、それがどの程度発生したかを判断できるようにすることは有益でしょう。組合せ対称交差検証法(Combinatorially Symmetrical Cross Validation、CSCV)は、David H. Baileyらによって書かれた学術論文「The Probability of Backtest Overfitting」で紹介されている手法で、戦略のパラメータを最適化する際に、過剰学習の程度を推定するために使用することができます。
この記事では、MQL5におけるCSCVの実装を実演し、それをエキスパートアドバイザー(EA)にどのように適用できるかを例を通して紹介します。
CSCV法
このセクションでは、CSCVの正確な方法について、選択したパフォーマンス基準に関連して収集する必要があるデータに関する予備的な側面から段階的に説明します。
CSCV法は戦略策定や分析以外のさまざまな領域で応用できますが、本稿では戦略最適化の文脈にこだわります。パラメータの設定を変えながら何度もテストをおこない、微調整が必要なパラメータのセットで定義された戦略がある場合です。
計算に着手する前に、まずどのようなパフォーマンス基準で戦略を評価するかを決める必要があります。CSCV法は、どのようなパフォーマンス指標でも使用できるという点で柔軟です。単純な利益から比率に基づく指標まで、CSCVには何の影響もありません。
選択された性能基準は、計算に使用される基礎データも決定します。これは、すべてのテスト走行から収集される生の粒状データです。例えば、パフォーマンス指標としてシャープレシオを使用することに決めた場合、各テスト実行からバーごとのリターンを収集する必要があります。単純な利益を使うのであれば、バーごとの損益が必要になります。重要なのは、各走行で収集されるデータ量が一定であることを確認することです。これにより、すべてのテスト実行において、対応する各データポイントの測定値を確保することができます。
- 最初のステップは、最適化中にさまざまなパラメータのバリエーションをテストするためのデータ収集から始まります。
- 最適化が完了したら、テストランから収集したすべてのデータを行列にプールします。この行列の各行には、対応するテスト実行の取引パフォーマンス指標を計算するために使用される、バーごとのパフォーマンス値がすべて含まれます。
- 行列は、試行されたパラメータの組み合わせと同じ数の行を持ち、列の数は全試験期間を構成するバー数と同じになります。これらの列は、任意の偶数のセットに分割されます。例えば、Nセットとします。
- これらの集合は部分行列であり、N/2の大きさの集団の組み合わせを形成するのに使われます。組合せ的には、一度にN/2ずつ、つまりNCn/2の合計N組合せを作ります。これらの組み合わせのそれぞれから、N/2個の部分行列を組み合わせてサンプル内集合(ISS)を構成し、さらにISSに含まれない残りの部分行列から対応するサンプル外集合(OOSS)を構成します。
- ISS行列とOOSS行列の各行について、対応するパフォーマンス指標を計算します。そして、ISS行列で最高のパフォーマンスを示した行にご注目ください。これは最適なパラメータ構成を表しています。OOSS行列の対応する行は、最適なパラメータ構成で達成された性能と比較して劣る性能を持つサンプル外のパラメータ試行の数をカウントすることにより、相対順位を計算するために使用されます。そしてこのカウントを、テストした全パラメータセットに対する割合として示します。
- すべての組み合わせの中で、相対順位の値が0.5以下である数を累積します。これは、最適なパラメータセットを使用して観測されたパフォーマンスを下回る、サンプル外のパラメータ構成の数です。すべての組み合わせが処理されると、この数字はすべての組み合わせ+1の端数として表示されます。バックテスト過剰学習(PBO)の確率を表します。
以下は、N=4の場合のステップを視覚化したものです。
この後のセクションでは、今説明したステップをコードで実装する方法を見てみましょう。本稿では、主にCSCVの中核となる手法を扱い、データ収集に関連するコードについては、本稿の最後に示す例に譲ります。
CSCVのMQL5実装
CSCV.mqhに含まれるCcsvcクラスは、CSCVアルゴリズムをカプセル化しています。CSCV.mqhは、MQL5の数学標準ライブラリのサブ関数をインクルードするところから始まります。
//+------------------------------------------------------------------+ //| CSCV.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include <Math\Stat\Math.mqh>
Criterion関数ポインタは、入力として配列が与えられたときにパフォーマンス指標を計算するために使用される関数型を定義します。
#include <Math\Stat\Math.mqh> typedef double (*Criterion)(const double &data[]); // function pointer for performance criterion
Ccscvには、ユーザーが慣れる必要がある方法が1つだけあります。クラスのインスタンスが初期化された後に呼び出すことができます。このメソッドCalculateProbabilty()は、成功時にPBO値を返します。エラーが発生した場合、メソッドは-1を返します。入力パラメータの説明は以下の通りです。
//+------------------------------------------------------------------+ //| combinatorially symmetric cross validation class | //+------------------------------------------------------------------+ class Cscv { ulong m_perfmeasures; //granular performance measures ulong m_trials; //number of parameter trials ulong m_combinations; //number of combinations ulong m_indices[], //array tracks combinations m_lengths[], //points to number measures for each combination m_flags []; //tracks processing of combinations double m_data [], //intermediary holding performance measures for current trial is_perf [], //in sample performance data oos_perf []; //out of sample performance data public: Cscv(void); //constructor ~Cscv(void); //destructor double CalculateProbability(const ulong blocks, const matrix &in_data,const Criterion criterion, const bool maximize_criterion); };
- 最初の入力パラメータはblocksです。これは、行列の列が分割されるセットの数(Nセット)に相当します。
- in_dataは、最適化実行のために試行されたパラメータバリエーションの総数と同じ数の行と、最適化のために選択された履歴全体を構成するバーと同じ数の列を持つ行列です。
- criterionは、選択されたパフォーマンス指標を計算するために使用されるルーチンへの関数ポインタです。このルーチンはdouble型の値を返し、double型の配列を入力とします。
- maximize_criterionはcriterionと関連しており、選択されたパフォーマンス指標のベストが最大値で定義されるのか最小値で定義されるのかを指定することができます。例えば、パフォーマンス基準としてドローダウンを使用する場合、ベストは最小値であるため、maximize_criterionはfalseでなければなりません。
double Cscv::CalculateProbability(const ulong blocks, const matrix &in_data,const Criterion criterion, const bool maximize_criterion) { //---get characteristics of matrix m_perfmeasures = in_data.Cols(); m_trials = in_data.Rows(); m_combinations=blocks/2*2; //---check inputs if(m_combinations<4) m_combinations = 4; //---memory allocation if(ArrayResize(m_indices,int(m_combinations))< int(m_combinations)|| ArrayResize(m_lengths,int(m_combinations))< int(m_combinations)|| ArrayResize(m_flags,int(m_combinations))<int(m_combinations) || ArrayResize(m_data,int(m_perfmeasures))<int(m_perfmeasures) || ArrayResize(is_perf,int(m_trials))<int(m_trials) || ArrayResize(oos_perf,int(m_trials))<int(m_trials)) { Print("Memory allocation error ", GetLastError()); return -1.0; } //---
ComputeProbabilityでは、まずin_data行列の列と行の数を取得し、それが偶数であることを確認するためにblocksを確認します。入力行列の次元を得ることは、内部インスタンスバッファのサイズを決定するために必要です。
int is_best_index ; //row index of oos_best parameter combination double oos_best, rel_rank ; //oos_best performance and relative rank values //--- ulong istart = 0 ; for(ulong i=0 ; i<m_combinations ; i++) { m_indices[i] = istart ; // Block starts here m_lengths[i] = (m_perfmeasures - istart) / (m_combinations-i) ; // It contains this many cases istart += m_lengths[i] ; // Next block } //--- ulong num_less =0; // Will count the number of time OOS of oos_best <= median OOS, for prob for(ulong i=0; i<m_combinations; i++) { if(i<m_combinations/2) // Identify the IS set m_flags[i]=1; else m_flags[i]=0; // corresponding OOS set } //---
内部バッファのメモリ割り当てが完了したら、m_combinationsに従って列のパーティショニングの準備を始めます。m_indices配列は、特定のパーティションの開始列インデックスで満たされ、m_lengthsは、それぞれに含まれる列の対応する数を保持します。num_lessは、サンプル内最良の試行のサンプル外パフォーマンスが、残りの試行のサンプル外パフォーマンスより小さい回数のカウントを保持します。m_flagsは、1または0のいずれかを含むことができる整数配列です。これは、すべての可能な組み合わせを反復するときに、サンプル内とサンプル外として指定されたサブセットを識別するのに役立ちます。
ulong ncombo; for(ncombo=0; ; ncombo++) { //--- in sample performance calculated in this loop for(ulong isys=0; isys<m_trials; isys++) { int n=0; for(ulong ic=0; ic<m_combinations; ic++) { if(m_flags[ic]) { for(ulong i=m_indices[ic]; i<m_indices[ic]+m_lengths[ic]; i++) m_data[n++] = in_data.Flat(isys*m_perfmeasures+i); } } is_perf[isys]=criterion(m_data); } //--- out of sample performance calculated here for(ulong isys=0; isys<m_trials; isys++) { int n=0; for(ulong ic=0; ic<m_combinations; ic++) { if(!m_flags[ic]) { for(ulong i=m_indices[ic]; i<m_indices[ic]+m_lengths[ic]; i++) m_data[n++] = in_data.Flat(isys*m_perfmeasures+i); } } oos_perf[isys]=criterion(m_data); }
この時点で、サンプル内セットとサンプル外セットのすべての組み合わせを反復するメインループが始まります。2つの内部ループが使用され、criterion関数を呼び出してサンプル内とサンプル外のパフォーマンスをシミュレートし、この値をそれぞれis_perf配列とos_perf配列に保存します。
//--- get the oos_best performing in sample index is_best_index = maximize_criterion?ArrayMaximum(is_perf):ArrayMinimum(is_perf); //--- corresponding oos performance oos_best = oos_perf[is_best_index];
maximize_criterionに従って、is_perf配列のベストパフォーマンス値のインデックスが計算されます。対応するサンプル外のパフォーマンス値はoos_best変数に保存されます。
//--- count oos results less than oos_best int count=0; for(ulong isys=0; isys<m_trials; isys++) { if(isys == ulong(is_best_index) || (maximize_criterion && oos_best>=oos_perf[isys]) || (!maximize_criterion && oos_best<=oos_perf[isys])) ++count; }
os_perf配列をループし、os_bestが同等かそれ以上である回数を数えます。
//--- calculate the relative rank rel_rank = double (count)/double (m_trials+1); //--- cumulate num_less if(rel_rank<=0.5) ++num_less;
カウントは相対順位を計算するために使われます。最後に、計算された相対順位が0.5未満の場合、num_lessが累積されます。
//---move calculation on to new combination updating flags array along the way int n=0; ulong iradix; for(iradix=0; iradix<m_combinations-1; iradix++) { if(m_flags[iradix]==1) { ++n; if(m_flags[iradix+1]==0) { m_flags[iradix]=0; m_flags[iradix+1]=0; for(ulong i=0; i<iradix; i++) { if(--n>0) m_flags[i]=1; else m_flags[i]=0; } break; } } }
最後の内部ループは、反復を次のサンプル内データセットとサンプル外データセットに移すために使用されます。
if(iradix == m_combinations-1) { ++ncombo; break; } } //--- final result return double(num_less)/double(ncombo); }
最後のifブロックは、num_lessをncomboで割って最終的なPBO値を返す前に、メインの外側ループから抜け出すタイミングを決定します。
Ccscvクラスの適用例を見る前に、このアルゴリズムが特定の戦略について何を明らかにするのか、時間をかけて検討する必要があります。
結果の解釈
実装したCSCVアルゴリズムは、単一のメトリックを出力します。すなわちPBOです。David H. Baileyらによると、PBOは、サンプル内データセットで最適化中に最高の性能を出したパラメータセットが、サンプル外データセットで最適でないパラメータセットを使った性能結果の中央値を下回る性能を達成する確率を定義しています。
この値が大きいほど、過剰学習の度合いが大きくなります。言い換えれば、サンプル外で戦略を適用すると、パフォーマンスが低下する可能性が高くなります。理想的なPBOは0.1以下でしょう。
達成されるPBO値は、主に最適化中に試行されるパラメータセットの多様性に依存します。選択されたパラメータセットが、現実的に実世界で適用されうるパラメータセットを代表するものであることを確認することが重要です。選ばれる可能性が低い、あるいは最適から近い、あるいは遠い組み合わせに支配されているパラメータの組み合わせを意図的に含めても、最終的な結果を汚すだけです。
例
このセクションでは、EAへのCcscvクラスの応用を紹介します。MetaTrader 5に同梱されている移動平均EAが、PBOの計算ができるように変更されます。CSCV法を効果的に実施するために、フレームを採用し、バーごとのデータを収集します。最適化が完了すると、各パスのデータは行列に照合されます。つまり、最低でもとOnTesterDeinit()をEAのコードに追加する必要があります。最後に、選択したEAは、ストラテジーテスターの低速&完全アルゴリズムオプションを使用して、完全な最適化をおこなう必要があります。
//+------------------------------------------------------------------+ //| MovingAverage_CSCV_DemoEA.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 <Returns.mqh> #include <CSCV.mqh> #include <Trade\Trade.mqh>
まず、CSCV.mqhとCReturnsクラスの定義を含むReturns.mqhから始めます。CReturnsは、シャープレシオ、平均リターン、またはトータルリターンを計算することができるバーごとのリターンを収集するのに便利です。最適なパフォーマンスを決定する基準として、このどちらかを使うことができます。記事の冒頭ですでに述べたとおりです。どのパフォーマンス指標を選んでも構いません。
sinput uint NumBlocks = 4;
NumBlocksという最適化不可能なパラメータが追加され、CSCVアルゴリズムが採用するパーティション数を指定します。後ほど、このパラメータの変更がPBOに影響することを確認します。
CReturns colrets;
ulong numrows,numcolumns;
CReturnsのインスタンスはグローバルに宣言されます。 ここではnumrowsとnumcolumnsも宣言されており、行列を初期化するために使用します。
//+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ void OnTesterInit() { numrows=1; //--- string name="MaximumRisk"; bool enable; double par1,par1_start,par1_step,par1_stop; ParameterGetRange(name,enable,par1,par1_start,par1_step,par1_stop); if(enable) numrows*=ulong((par1_stop-par1_start)/par1_step)+1; //--- name="DecreaseFactor"; double par2,par2_start,par2_step,par2_stop; ParameterGetRange(name,enable,par2,par2_start,par2_step,par2_stop); if(enable) numrows*=ulong((par2_stop-par2_start)/par2_step)+1; //--- name="MovingPeriod"; long par3,par3_start,par3_step,par3_stop; ParameterGetRange(name,enable,par3,par3_start,par3_step,par3_stop); if(enable) numrows*=ulong((par3_stop-par3_start)/par3_step)+1; //--- name="MovingShift"; long par4,par4_start,par4_step,par4_stop; ParameterGetRange(name,enable,par4,par4_start,par4_step,par4_stop); if(enable) numrows*=ulong((par4_stop-par4_start)/par4_step)+1; }
OnTesterInit()」ハンドラを追加し、その中でテストされるパラメータセットの数をカウントします。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- colrets.OnNewTick(); //--- if(SelectPosition()) CheckForClose(); else CheckForOpen(); //--- }
OnTick()イベントハンドラで、CReturnsのOnNewtick()メソッドを呼び出します。
//+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { //--- double ret=0.0; double array[]; //--- if(colrets.GetReturns(ENUM_RETURNS_ALL_BARS,array)) { //--- ret = MathSum(array); if(!FrameAdd(IntegerToString(MA_MAGIC),long(MA_MAGIC),double(array.Size()),array)) { Print("Could not add frame ", GetLastError()); return 0; } //--- } //---return return(ret); }
OnTester()の内部で、グローバルに宣言されたCReturnsインスタンスで戻り値の配列を収集します。そして最後に、FrameAdd()を呼び出して、このデータをフレームに追加します。
//+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { //---prob value numcolumns = 0; double probability=-1; int count_frames=0; matrix data_matrix=matrix::Zeros(numrows,1); vector addvector=vector::Zeros(1); Cscv cscv; //---calculate if(FrameFilter(IntegerToString(MA_MAGIC),long(MA_MAGIC))) { //--- ulong pass; string frame_name; long frame_id; double passed_value; double passed_data[]; //--- while(FrameNext(pass,frame_name,frame_id,passed_value,passed_data)) { //--- if(!numcolumns) { numcolumns=ulong(passed_value); addvector.Resize(numcolumns); data_matrix.Resize(numrows,numcolumns); } //--- if(addvector.Assign(passed_data)) { data_matrix.Row(addvector,pass); count_frames++; } //--- } } else Print("Error retrieving frames ", GetLastError()); //---results probability = cscv.CalculateProbability(NumBlocks,data_matrix,MathSum,true); //---output results Print("cols ",data_matrix.Cols()," rows ",data_matrix.Rows()); Print("Number of passes processed: ", count_frames, " Probability: ",probability); //--- }
EAに追加されたものの大部分はOnTesterDeinit()にあります。ここでCcscvのインスタンスを行列とベクトル型の変数とともに宣言します。すべてのフレームをループし、そのデータを行列に渡します。ベクトルは、各フレームの新しいデータ行を追加するための仲介として使用されます。
CcscvのCalculateProbability()メソッドは、結果をターミナルのExpertsタブに出力する前に呼び出されます。この例では、MathSum()関数をメソッドに渡しています。つまり、トータルリターンが最適なパラメータセットを決定するために使われます。出力はまた、すべてのデータがキャプチャされたことを確認するために、処理されたフレーム数を示します。
以下は、修正したEAを様々な設定で実行した結果です。時間枠がは異なります。PBOの結果はターミナルの[エキスパート]タブに出力されます。
MovingAverage_CSCV_DemoEA (EURUSD,H1) Number of passes processed: 23520 Probability: 0.3333333333333333
NumBlocks | TimeFrame | バックテスト過剰学習の確率 |
---|---|---|
4 | 週次 | 0.3333 |
4 | 日次 | 0.6666 |
4 | 12時間 | 0.6666 |
8 | 週次 | 0.2 |
8 | 日次 | 0.8 |
8 | 12時間 | 0.6 |
16 | 週次 | 0.4444 |
16 | 日次 | 0.8888 |
16 | 12時間 | 0.6666 |
最高の結果はPBOの0.2です。他はもっとひどかったです。このことは、このEAがサンプル外のデータセットに適用された場合、パフォーマンスが低下する可能性が非常に高いことを示しています。また、こうしたPBOスコアの悪さは、異なる時間枠でも持続していることがわかります。分析に使用するパーティション数を調整しても、当初悪かったスコアは改善されませんでした。
結論
最適化手順後の過剰学習を評価するために、組合せ対称的交差検証技術の実装を実証しました。モンテカルロ順列を使って過剰学習を定量化するのに比べて、CSCVには
比較的速いという利点があります。また、利用可能な過去のデータを効率的に活用することもできます。とはいえ、練習生が注意すべき落とし穴もあります。この方法の信頼性は、使用される基礎データのみに依存します。
特に、試行されたパラメータバリエーションの範囲についてです。パラメータのバリエーションが少ないと過剰学習の過小推定になり、同時に非現実的なパラメータの組み合わせを多く含むと過大推定になる可能性があります。また、最適化期間として選択された時間枠にも注意が必要です。これはストラテジーに適用するパラメータの選択に影響を与える可能性があります。つまり、最終的なPBOは異なる時間枠の中で変化しうるということです。一般的に言って、テストでは可能な限り多くの実行可能なパラメータ構成を考慮すべきです。
このテストの特筆すべき欠点は、ソースコードにアクセスできないEAには簡単に適用できないことです。理論的には、可能性のあるパラメータ構成ごとに個別のバックテストを実行することは可能ですが、モンテカルロ法を採用するのと同じような退屈さが生じます。
CSCVとPBOの解釈についてのより詳細な説明については、この記事の2段落目にある原著論文をご参照ください。記事で紹介したすべてのプログラムのソースコードを以下に添付します。
ファイル名 | 詳細 |
---|---|
Mql5\Include\Returns.mqh | リターンや株式データをリアルタイムで収集するためのCReturnsクラスを定義 |
Mql5\Include\CSCV.mqh | 組合せ対称相互検証を実装するCcscvクラスの定義を含む |
Mql5\Experts\MovingAverage_CSCV_DemoEA.mq5 | Ccscvクラスの適用を示す修正移動平均EA |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13743





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索