English Deutsch
preview
共和分株式による統計的裁定取引(第9回):バックテストポートフォリオのウェイト更新

共和分株式による統計的裁定取引(第9回):バックテストポートフォリオのウェイト更新

MetaTrader 5トレーディングシステム |
15 0
Jocimar Lopes
Jocimar Lopes

はじめに

「市場は絶えず変化し続けている。」これは、平均的な個人トレーダー向けの統計的裁定取引フレームワークを構築する上での基本的な考え方です。私たちは、弱気相場や強気相場、方向トレンド、相関資産といった一般的な概念を避けることで、この方針を採用しています。その代わりに、統計的手法を用いて、資産のペアまたはグループが、一定期間にわたり関係性が維持される確率を推定します。今のところ、金融市場における柔軟性とほぼ普遍的な適用性という観点から、共和分関係を取り上げています。異なるクラスの資産を含むあらゆる資産間の共和分関係、さらには金融資産と非金融データ(たとえば、株価と輸送コストの推移など)間の共和分関係を調べることができます。共和分関係が発見されれば、取引機会が得られる可能性が高くなります。

欠点は、統計的な観点から見ると、共和分関係が今後1時間、1日、あるいは1週間有効であり続けるという保証がないことです。次のティックから関係が崩れる可能性は常に存在します。ティッカーの価格が変動する確率はほぼ100%です。

私たちは、その時点の銘柄価格から共和分ベクトルを計算します。共和分ベクトルから、相対的なポートフォリオウェイト、つまり各注文で売買される資産の数量が得られます。銘柄の価格は常に変動しているため、ポートフォリオにおける各銘柄の比率も変動します。ほぼ確実に言えるのは、昨日最適だった注文量は今日は最適ではないということです。相対価格が変動したため、ポートフォリオのウェイトを更新する必要があります。

前回の記事では、インサンプル/アウトオブサンプルADF (IS/OOS ADF)検証とローリングウィンドウ固有ベクトル比較(RWEC, Rolling Windows Eigenvector Comparison)を組み合わせて使用することで、ポートフォリオウェイトの安定性を継続的に監視する方法を見てきました。このよく知られた手法は、過去の共和分関係の破綻を検出し、将来的な資産間関係の崩壊確率を推定するのに有効です。これらの特性により、ポートフォリオ構築におけるデータ分析の文脈でも、ライブトレーディングの監視におけるリスク管理ツールとしても有用です。ポートフォリオ構築時には、各手法の主要パラメータを変化させながらモデルのパフォーマンスを評価できる一方、モニタリング時には、最新のデータ分析結果に基づいてこれらのパラメータの調整が可能です。また、RWECの実装では、共和分の強さをランキングするスコアリングシステムと同じ手法であるジョハンセンの共和分検定を使用しているため、そこから得られる共和分ベクトルをポートフォリオのウェイト更新に活用することができます。

実際の取引では、EAは「strategy」テーブルからポートフォリオのウェイトを読み込みます。これは、データベースを単一の信頼できる情報源として設定した際に説明した通りです。しかし、バックテストではデータベースにアクセスすることができません。バックテストは、数十から場合によっては数百に及ぶ資産ペアやバスケットに対して、これらのパラメータの微調整を現実的な時間内でシミュレーションできる唯一の実用的な方法です。バックテストをおこなうことで、シグナルの安定性テスト手法の評価や、リバランスアルゴリズムの有効性を検証することができます。また、リバランスに関するEAのロジックが期待通りに動作しているか、さらに選択したリバランスパラメータがどの程度パフォーマンス改善に寄与するかを確認する必要があります。Metatrader 5のストラテジーテスターでデータベースのデータにアクセスする最もシンプルな方法は、必要なデータをファイルとしてエクスポートし、EAから直接読み込むことです。ここでは、「strategy」テーブル全体をCSV(カンマ区切り)ファイルとしてエクスポートし、専用のテスト用ヘルパー関数で「新しい」ポートフォリオウェイトを読み込みます。


データベースにサンプルデータを追加する

データベースの「strategy」テーブルをエクスポートする前に、RWECデータを格納しておく必要があります。そのために、RWEC分析を実行し、その結果をデータベースへ保存するシンプルなPythonスクリプトを使用します。なお、これはバックテスト用のシミュレーションである点に注意してください。通常の運用では、RWEC分析は実際の取引の監視プロセスの一部として日次で実行され、その結果はEAによってデータベースから直接利用されます。

このスクリプトは、MetaTrader 5ターミナルから必要なデータを取得します。これまでの例と同様に、ここではMetaQuotesのデモ口座で利用可能な銘柄のみを使用しているため、ご自身で実行する際も比較的簡単に再現できるはずです。

スクリプトはrwec2db.pyという名前で添付されています。 

図1:rwec2db.py内のメソッド/関数の構成を折りたたみ表示で確認した画面

図1:rwec2db.py内のメソッド/関数の構成を折りたたみ表示で確認した画面

スクリプトを実行すると、次のような出力が表示されるはずです。

図2:rwec2db.pyの期待される出力を示す画面

図2:rwec2db.pyの期待される出力を示す画面

共和分ベクトルが見つからなかった場合は、その旨が通知されます。

図3:共和分ベクトルが見つからない場合の rwec2db.pyの出力画面

図3:共和分ベクトルが見つからない場合のrwec2db.pyの出力画面

上記の状況は、同一銘柄を比較的短期間(例:30バー程度)でテストした場合に発生することがあります。これは、RWECを実行するための履歴データが十分でないためです。この場合は、より多くのデータを取得するか、ローリングウィンドウの長さを増やすことで対応できます。すべてが正常に動作した場合、「strategy」テーブルは次のようになります。

図4:MetaEditor内蔵SQLiteデータベース:「strategy」テーブル(サンプルデータ格納済み)

図4:MetaEditor内蔵SQLiteデータベース:「strategy」テーブル(サンプルデータ格納済み)

これでテーブルをエクスポートする準備が整いました。


データベーステーブルのエクスポート

MetaEditorには、テーブルのエクスポート機能が組み込まれています。テーブル名を右クリックしてください。

図5:MetaEditorデータベースのコンテキストメニュー画面

図5:MetaEditorデータベースのコンテキストメニュー画面

エクスポートしたファイルが添付のサンプルコードと完全に互換性を持つようにするには、次のダイアログボックスで以下のオプションを選択してください。

図6:MetaEditorデータベースのエクスポートオプションダイアログ画面(推奨オプションを強調表示)

図6:MetaEditorデータベースのエクスポートオプションダイアログ画面(推奨オプションを強調表示)

区切り文字としてタブ文字を選択します。実際には、タブ区切り値ファイル(TSV)を使用します。これは、ポートフォリオのウェイトがデータベースにJSON配列として保存されているため、ウェイトの解析を容易にするためのものです。これらの配列には既にカンマが含まれています。 

[1.0, -0.045459, 0.021855, -0.033486]

タブ文字を区切り文字として選択することで、この解析を容易にすることができます。また、列名と二重引用符で囲まれた文字列の保持を選択します。

TSVファイルがテスター環境からアクセスできるようにするには、TERMINAL_DATA_PATH(「ターミナルデータが保存されるフォルダ」)または TERMINAL_COMMONDATA_PATH(「コンピュータにインストールされているすべてのターミナルの共通パス」)に保存する必要があることに注意してください。この例では、前者を使用します。

TSVファイルを上記で述べた共通パス以外に保存する場合は、メインのMQL5ファイルにプロパティtester_fileを含める必要があります。

//+------------------------------------------------------------------+
//|                                                  CointNasdaq.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Expert Advisor - Cointegration Statistical Arbitrage             |
//| Assets: Dynamic assets allocation                                |
//| Strategy: Mean-reversion on Johansen cointegration portfolio     |
//+------------------------------------------------------------------+
#property tester_file "StatArb\\strategy_202512041731.csv"

「インクルードファイルに記載されているプロパティは完全に無視されます。プロパティはメインのmq5ファイルで指定する必要があります。(…)拡張子を示すテスターのファイル名(二重引用符で囲む定数文字列)。指定されたファイルはテスターに渡されます。テスト対象となる入力ファイルが必要な場合は、必ず指定する必要があります。」(MQL5ドキュメント

このプロパティがないと、テスター環境からファイルを読み込むことができません。

図7:ストラテジーテスターでのファイルオープンエラー(Metatrader 5操作ログでの表示)

図7:ストラテジーテスターでのファイルオープンエラー(Metatrader 5操作ログでの表示)

理由は、セキュリティ上の理由から、テスター環境が隔離されたサンドボックスとして機能しているということです。この記事では、テスターでファイルを操作する際の内部プロセスについて、簡潔かつ明確に説明しています。 MetaTrader 5のドキュメントには、ファイルの読み書きに関する情報が豊富に記載されています。AlgoBookにはMQL5でファイルを操作する方法に関する包括的なガイドが掲載されています。ここでは、特定のユースケースにおける要件に焦点を当てます。


ファイルから戦略パラメータを読み込む

準備が整ったので、LoadStrategyFromFile()関数を紹介します。この関数は、この記事の下部に添付されているサンプルEA CointNasdaq.mq5に含まれています。添付のコードは意図的に「未加工」の状態です。デバッグ用の出力を削除して、内容を装飾したわけではありません。その代わりに、私が執筆する際に参考にしたすべてのコメントをそのまま残しておきました。そうすることで、皆さんがより理解しやすくなり、よりシンプルで分かりやすく、効率的な解決策を探ることができるでしょう。使用したデバッグ出力のほとんどはコメントアウトされています。したがって、ご自身で変更を加えたコードをテストする際に問題が発生した場合は、必要な箇所のコードのコメントを解除するだけで済みます。

LoadStrategyFromFile()関数は、実行環境によっては、EAのOnInit()イベントハンドラ内で直接呼び出すことができます。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ResetLastError();
// Check if all symbols are available
   for(int i = 0; i < ArraySize(symbols); i++)
     {
      if(!SymbolSelect(symbols[i], true))
        {
         Print("Error: Symbol ", symbols[i], " not found!");
         return(INIT_FAILED);
        }
     }
// Initialize spread buffer
   ArrayResize(spreadBuffer, InpLookbackPeriod);
// Set a timer for spread, mean, stdev calculations
// and strategy parameters update (check DB)
   EventSetTimer(InpUpdateFreq * 60); // min one minute
// check if we are backtesting
   if(!MQLInfoInteger(MQL_TESTER))
     {
      // Load strategy parameters from database
      if(!LoadStrategyFromDB(InpDbFilename,
                             InpStrategyName,
                             symbols,
                             weights,
                             timeframe,
                             InpLookbackPeriod))
        {
         // Handle error - maybe use default values
         printf("Error at " + __FUNCTION__ + " %s ",
                getUninitReasonText(GetLastError()));
         return INIT_FAILED;
        }
     }
   else
     {
      // Load strategy parameters from CSV file
      if(!LoadStrategyFromFile(InpTesterStrategyFilename, symbols, weights))
        {
         // Handle error - maybe use default values
         printf("Error at " + __FUNCTION__ + " %s ",
                getUninitReasonText(GetLastError()));
         return INIT_FAILED;
        }
     }
   return(INIT_SUCCEEDED);
  }

EAがテスター環境で実行されている場合、ファイルからストラテジーを読み込みます。通常の取引では、戦略はデータベースから読み込まれます。

EAのメインファイルが煩雑になるのを避けるため、この機能は付属のヘッダファイルであるTestHelper.mqhに実装されており、ここにも添付されています。

//+------------------------------------------------------------------+
//|  Load the strategy parameters from CSV/TSV file                  |
//+------------------------------------------------------------------+
bool LoadStrategyFromFile(string filename,
                          string &strat_symbols[],
                          double &strat_weights[])
 {
   Print("Running on tester");
// Instantiate the hash map
   CHashMap<ulong, CArrayDouble*> updates;
// Load the weights from the CSV file
   LoadWeights(filename, updates);
   Print("Updates count ", updates.Count());
(...)

関数のパラメータを見ると、この関数はポートフォリオのウェイトのみを扱っていることがわかります。これは、データベースから戦略を読み込み、戦略名、期間、およびルックバック期間を含める、その対となる機能とは対照的です。それは、この実装が現在開発中だからです。後々、ポートフォリオのローテーションを扱う際には、すべての戦略パラメータも含まれるようになります。

この関数は、MQL5標準ライブラリ/汎用データコレクションから動的ハッシュテーブルをインスタンス化することから始まります。これはCHashMapで、キーにはポートフォリオの更新タイムスタンプが格納され、値にはdouble型の配列としてポートフォリオのウェイトが格納されます。オブジェクトインスタンスは、ファイル名とともに、CSVファイルを正しく読み込むための専用関数LoadWeights()への参照として渡されます。

//+------------------------------------------------------------------+
//|    Load portfolio weights updates from CSV/TSV file              |
//+------------------------------------------------------------------+
void LoadWeights(string filename,
                 CHashMap<ulong, CArrayDouble*> &updates)
  {
   ResetLastError();
   int filehandle = FileOpen(filename,
                             FILE_ANSI | FILE_CSV | FILE_READ, '\\t', CP_ACP);
   if(filehandle != INVALID_HANDLE)
     {
      printf("Data Path: %s Filename: %s", TerminalInfoString(TERMINAL_DATA_PATH), filename);
      // Read and discard the header line
      string first_line = FileReadString(filehandle);
      Print(first_line);
(...)

LoadWeights()関数は、MQL5でテキストファイルを読み込む際の一般的な方法に従います。繰り返しになりますが、ファイルを開く際にFILE_COMMONフラグを使用していないことに注意してください。つまり、ターミナルデータパスを使用しているため、上記のようにEAで#property tester_fileを使用する必要があります。最初の行のCSVヘッダは破棄し、確認のために操作ログに出力します。

図8:Metatrader 5操作ログ(テストログの最初の行を表示)

図8:Metatrader 5操作ログ(テストログの最初の行を表示)

次に、ヘッダ以外のCSVファイルの行をタブ文字で分割するために、反復処理を開始します。結果として得られるセグメントが、私たちが求めている値です。そこで、それらをfields[]文字列配列に格納します。

// iterate over lines
      while(!FileIsEnding(filehandle))
        {
         string line = FileReadString(filehandle);
         string fields[]; // fields[0] -> tstamp   fields[4] -> weights
         int count = StringSplit(line, '\t', fields);
         //printf("fields => %s  %s %s %s %s %s %s",
         //       fields[0], fields[1], fields[2],
         //       fields[3], fields[4], fields[5], fields[6]);
         //—
(...)

各読み取り行のウェイト配列を受け取るために、CArrayDoubleオブジェクトを作成します。これは、CHashMapクラスが必要とするオブジェクトです。

// Create the CArrayDouble object for this timestamp
         CArrayDouble *current_weights_arr = new CArrayDouble();
         ulong tstamp = 0;
(...)

ポートフォリオウェイトの配列はデータベースにJSON配列として保存されているため、CArrayDoubleオブジェクトに渡す前に、括弧を削除して配列を整理する必要があります。

// Ensure we have at least the tstamp (0) and weights (4) fields
         if(count > 4)
           {
            // weights string
            string weights_str = fields[4];
            StringReplace(weights_str, "[", "");
            StringReplace(weights_str, "]", "");
            // weights strings array
            string weights_str_arr[];
            int weights_count = StringSplit(weights_str, ',', weights_str_arr);
            //---
            if(current_weights_arr == NULL)
              {
               Print("Err creating CArrayDouble for timestamp ", fields[0]);
               continue; // Skip to the next line
              }
            // Populate the new CArrayDouble
            for(int i = 0; i < weights_count; i++)
              {
               //printf("weights_str_arr %s", weights_str_arr[i]);
               double weight_value = StringToDouble(weights_str_arr[i]);
               //printf("weight_value %.6f", weight_value);
               tstamp = (ulong)StringToInteger(fields[0]);
               //printf("tstamp %I64u ", tstamp);
               current_weights_arr.Add(weight_value);
               //printf("current_weights_arr Total: %d", current_weights_arr.Total());
              }
(...)

CArrayDoubleオブジェクトにデータが格納されたので、これをハッシュマップに追加できます。           

// 4. Add to the HashMap once per line
            if(updates.Add(tstamp, current_weights_arr))
              {
               printf("Added tstamp %I64u -> %s ", tstamp, TimeToString(tstamp));
              }
            else
              {
               Print("Failed adding record");
              }
           }
        }
     }
   else
     {
      printf("Error opening file %s. Error: %i", filename, GetLastError());
     }
   FileClose(filehandle);
  }

図9:Metatrader 5操作ログ(ハッシュマップに追加された更新のタイムスタンプを表示)

図9:Metatrader 5操作ログ(ハッシュマップに追加された更新のタイムスタンプを表示)

LoadStrategyFromFile()関数に戻り、読み込まれたポートフォリオ更新の数を確認します。これは、CSVファイルの行数から1行(ヘッダ行)を引いた数です。

// Load the weights from the CSV file
   LoadWeights(filename, updates);
   Print("Updates count ", updates.Count());
// copy the values to iterable arrays
   ulong tstamp_keys[];
   CArrayDouble *weights_values[];
   updates.CopyTo(tstamp_keys, weights_values);
// check if everything was copied
   Print("Keys size: ", tstamp_keys.Size());
   Print("Values size: ", weights_values.Size());
(...)

図10:Metatrader 5操作ログ(処理すべき更新の数を表示)

図10:Metatrader 5操作ログ(処理すべき更新の数を表示)

次に、ファイルに保存されている古い更新がないか確認します。 

// check for outdated updates on file
   ulong first_tstamp_on_file = tstamp_keys[0];
   printf("first_tstamp_on_file %I64u", first_tstamp_on_file);
   ulong update_to_apply = 0;
   if(FileHasOutdatedUpdates(first_tstamp_on_file))
      FileCleanUpdates(tstamp_keys, updates, update_to_apply);
(...)

実際の取引時にデータベースから取得するポートフォリオのウェイト更新をシミュレートするために、エクスポートされたデータベースデータを使用していることに注意してください。つまり、関連する更新は直近のもの、つまり、現在時刻の直前にある最も新しいタイムスタンプのものです。しかし、データをエクスポートする際、データベースには、バックテスト開始時刻の何日も前、あるいは何週間も前の古いRWECデータが含まれている可能性が高いです。サンプルデータとバックテスト設定の日時整合性を維持するために、これらの古いデータは削除する必要があります。

//+------------------------------------------------------------------+
//|   check if the CSV file has outdated updates                     |
//+------------------------------------------------------------------+
bool FileHasOutdatedUpdates(ulong updates_start_time)
  {
   datetime test_start_time = TimeCurrent();
   if((datetime)updates_start_time < test_start_time)
     {
      Print("Warning! Updates starts before test start time.");
      printf("Test start time: %s", TimeToString(test_start_time));
      printf("Updates start time: %s", TimeToString(updates_start_time));
      Print("Will REMOVE outdated updates.");
      return true;
     }
   return false;
  }

図11:Metatrader 5操作ログ(削除すべき古い更新の警告を表示)

図11:Metatrader 5操作ログ(削除すべき古い更新の警告を表示)

FileCleanUpdates()関数は、タイムスタンプを反復処理して、最後の更新を除くすべての古い更新を削除します。

//+------------------------------------------------------------------+
//| iterate over keys to remove outdated updates                     |
//+------------------------------------------------------------------+
void FileCleanUpdates(ulong &tstamp_keys[],
                      CHashMap<ulong, CArrayDouble*> &updates,
                      ulong &update_to_apply)
  {
   int outdated_count = 0;
   ulong outdated_keys[];
   for(int i = 0; i < ArraySize(tstamp_keys); i++)
     {
      if((datetime)tstamp_keys[i] < TimeCurrent()) // look for outdated updates
        {
         printf("Outdated updates at: %s", TimeToString(tstamp_keys[i]));
         outdated_keys.Push(tstamp_keys[i]);
         outdated_count++;
        }
      while(outdated_count > 1) // preserve the newest one to be applied
        {
         if(updates.Remove(tstamp_keys[i - 1]))
           {
            outdated_count--;
            printf("Removed outdated update from %s ", TimeToString(tstamp_keys[i - 1]));
           }
        }
     }
   printf("Removed %i outdated updates:", outdated_keys.Size() - 1);
   update_to_apply = outdated_keys[outdated_keys.Size() - 1];
   printf("Update from %s to be applied", TimeToString(update_to_apply));
  }

削除された古い更新と適用される更新は、いずれも操作ログに記載されています。

図12:Metatrader 5操作ログ(削除された更新の通知を表示)

図12:Metatrader 5操作ログ(削除された更新の通知を表示)

最後に、LoadStrategyFromFile()関数は、EAのポートフォリオのウェイトを設定することでその役割を完了します。

//---
   CArrayDouble *new_weights = new CArrayDouble();
   if(updates.TryGetValue(update_to_apply, new_weights))
     {
      ArrayResize(strat_weights, 4);
      //---
      strat_weights[0] = new_weights[0];
      strat_weights[1] = new_weights[1];
      strat_weights[2] = new_weights[2];
      strat_weights[3] = new_weights[3];
      //---
      ArrayResize(strat_symbols, 4);
      //---
      strat_symbols[0] = "INTC";
      strat_symbols[1] = "AMD";
      strat_symbols[2] = "AVGO";
      strat_symbols[3] = "MU";
      //---
      printf("New weights: %s %.6f | %s %.6f | %s %.6f | %s %.6f",
             strat_symbols[0], new_weights[0],
             strat_symbols[1], new_weights[1],
             strat_symbols[2], new_weights[2],
             strat_symbols[3], new_weights[3]
            );
      return true;
     }
   return false;
  }

新しいポートフォリオのウェイトは、操作ログに掲載されています。

図13:Metatrader 5操作ログ(新しいポートフォリオウェイトの通知を表示)

図13:Metatrader 5操作ログ(新しいポートフォリオウェイトの通知を表示) 

上記すべては、EAのOnInit()イベントハンドラから一度だけ呼び出されます。バックテストの開始時に、EAはCSVファイルから戦略パラメータを読み込み、ハッシュマップオブジェクトに格納し、バックテスト開始時刻より前のタイムスタンプを持つ更新(「古い更新」)があれば削除し、最新の更新を適用します。バックテストが進むにつれて、EAが実際の取引で実行されている間に発生するポートフォリオのウェイト更新をシミュレートするために、バックテストの開始時刻よりも後のタイムスタンプを持つ以降の更新を適用する必要があります。これは、OnTimer()イベントハンドラを使用しておこなわれます。

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   ResetLastError();
   if(!MQLInfoInteger(MQL_TESTER))
     {
      // Wrapper around LoadStrategyFromDB: for clarity
      if(!UpdateModelParams(InpDbFilename,
                            InpStrategyName,
                            symbols,
                            weights,
                            timeframe,
                            InpLookbackPeriod))
        {
         printf("%s failed: Error %i", __FUNCTION__, GetLastError());
        }
     }
   else
     {
      if(!UpdateModelParamsFromFile(symbols, weights))
        {
         printf("%s failed: Error %i", __FUNCTION__, GetLastError());
        }
     }
   printf("Actual weights: %s %.6f | %s %.6f | %s %.6f | %s %.6f",
          symbols[0], weights[0],
          symbols[1], weights[1],
          symbols[2], weights[2],
          symbols[3], weights[3]
         );

UpdateModelParamsFromFile()関数は非常にシンプルです。HashMapのキーを別の配列にコピーしたので、ここでは各関数呼び出し時にそれらを順番に取得するだけで済みます。各キーは更新のタイムスタンプです。現在時刻より前であれば、それに応じてバスケットのウェイトを更新します。それ以外の場合、キー/タイムスタンプが現在時刻よりも大きい場合は、更新はまだ存在しません。この場合、エラーは発生していないため関数はtrueを返しますが、更新は適用されません。HashMap<> TryGetValue() メソッドがハッシュマップオブジェクト内で該当するキーを見つけられない場合にのみ、false/エラーが返されます。

ulong tstamp_keys[];
CHashMap<ulong, CArrayDouble*> *updates = new CHashMap<ulong, CArrayDouble*>();

//+------------------------------------------------------------------+
//|  get the earlier tstamp on the updates hash map;                 |
//|  then update the model params (symbols and weights)              |
//|  with its values                                                 |
//+------------------------------------------------------------------+ 
bool UpdateModelParamsFromFile(string &curr_symbols[], double &curr_weights[])
  {
// Print("Updating model params from file");

// get the earlier tstamp on the updates hash map
   int static i = 0;
   if((datetime)tstamp_keys[i] < TimeCurrent())
     {
      curr_symbols[0] = "INTC";
      curr_symbols[1] = "AMD";
      curr_symbols[2] = "AVGO";
      curr_symbols[3] = "MU";
//—--
      CArrayDouble *new_weights = new CArrayDouble();
      if(!updates.TryGetValue(tstamp_keys[i], new_weights))
        {
         return false;
        }
      curr_weights[0] = new_weights[0];
      curr_weights[1] = new_weights[1];
      curr_weights[2] = new_weights[2];
      curr_weights[3] = new_weights[3];
//—-- increment the idx
      i++;
      delete new_weights;
     }
   else
     {
      Print("No update to apply");
     }
   return true;
  }

バックテストで更新が発生すると、操作ログには次のように記録されます。

図14:MetaTrader 5操作ログ(バックテストにおけるポートフォリオのウェイト更新を表示)

図14:MetaTrader 5操作ログ(バックテストにおけるポートフォリオのウェイト更新を表示)

バックテストは、データベースからエクスポートされたCSV/TSVファイルの読み込み、解析、およびクリーンアップが完了した後、ここから開始されます。

この記事の序論で述べたように、リバランスのためのEAロジックが期待どおりに機能しているかどうか、また、リバランスのために選択したRWECパラメータがどの程度結果を改善するかを確認する必要があります。


検定対象となるRWECパラメータ

バックテストの目的は、最適なローリング共和分パラメータ値に近づけることです。 RWECは、共和分関係の存在を評価するためにジョハンセン共和分検定を使用していることに注意してください。この検定には独自のパラメータがありますが、ここでは既に共和分関係にあるバスケットを扱っていることを前提としているため、それらのパラメータは考慮しません。RWECの役割は、パイプラインのスクリーニング/スコアリングステップでジョハンセン検定によって以前に計算されたポートフォリオのウェイトを更新することです。バックテストの対象となる主なRWECパラメータは次のとおりです。

  1. 要求された履歴データの時間軸
  2. 共和分検定ウィンドウの長さ
  3. 重なり合うウィンドウの長さ 

以下で各パラメータについて説明するように、これらのパラメータを調整する際には、常に適時性と正確性の間にトレードオフが存在します。ここでの目標は、適切なバランスに近づけることです。まず、RWEC指標を少なくとも3つのウィンドウサイズでバックテストし、H4時間足においてどのウィンドウサイズが最適なポートフォリオウェイトのリバランスを提供するかを確認します。これは、私たちが最初から取り組んできた期間です。評価基準は相対的ドローダウンとします。

警告:現時点では、最適なRWECパラメータを見つけるためにバックテストを実施しています。戦略の収益性には焦点を当てていません。すでに評価するための客観的な基準として、相対ドローダウンがあります。バスケットの最適なパラメータを選択したら、バックテストではなく、実際の取引でそれらを使用します。これがバックテストの目的であることを理解することが極めて重要です。


バックテスト設定

まずは、一般的な1年間、つまり約252営業日という期間から始めましょう。
図15:1年間のバックテスト(約252取引日)の設定画面

図15:1年間のバックテスト(約252取引日)の設定画面


RWECの時間軸(n_bars)は、バックテストの開始日とウィンドウの長さの合計以上である必要があります。バックテストの開始直後に早期更新をおこなうことで、実際の取引をより適切にシミュレートできます。H4時間足を使用しているため、株式の場合、1日に2本のH4足があり、少なくとも504本のバーが必要です。ウィンドウの長さごとに90本のバーを追加します。     

def fetch_data(self, symbols, timeframe=mt5.TIMEFRAME_H4, n_bars=504+90):
  """Fetch OHLC data from MT5"""

ヒント:エクスポートを容易にするため、RWECを実行する前に毎回「strategy」テーブルをクリアすることをお勧めします。この表は、データ分析とEAをつなぐ架け橋に過ぎないことを覚えておいてください。その唯一の目的は、実際の取引に最新の戦略パラメータを提供することです。データソースは、テキストファイルやWeb APIなど、あらゆる外部データソースが考えられます。このデータを保存したい場合は、バックテストにも一時テーブルを使用できます。ここで重要なのは、このデータは使い捨て可能であるという点です。

上記のn_bars、window=90、step=22でrwec2db.pyスクリプトを実行すると、「strategy」テーブルには次の内容が含まれるはずです。

図16:1年間のバックテストにおけるRWECベクトルを含む「strategy」テーブル
図16:1年間のバックテストにおけるRWECベクトルを含む「strategy」テーブル

上記のセクションで説明したようにこのテーブルをエクスポートすると、TERMINAL_DATA_PATHに次のようなTSVファイルが作成されます。

"tstamp"        "test_id"       "name"  "symbols"       "weights"       "timeframe"     "lookback"
1733155200      1       RWEC_CointNasdaq        INTC,AMD,AVGO,MU        [1.0, 0.19818, -0.385289, 0.29447]      H4      90
1734451200      1       RWEC_CointNasdaq        INTC,AMD,AVGO,MU        [1.0, -1.166557, -0.762914, 3.44909]    H4      90
(...)
1764360000      1       RWEC_CointNasdaq        INTC,AMD,AVGO,MU        [1.0, -0.072521, -0.063501, 0.023864]   H4      90

テストの開始日は2024年12月13日であり、RWECによって計算された最初のベクトルのタイムスタンプは1733155200です。これは2024年12月2日に相当します。これは、バックテスト開始予定日のほぼ2週間前です。つまり、バックテスト開始日の約2週間前の時点のデータとなります。これにより、すでに共和分ベクトルが存在した状態でシミュレーションを開始できるため、古いエントリを削除する必要はありません。

また、2つ目のベクトルのタイムスタンプは1734451200で、これは2024年12月17日に相当し、バックテスト開始日の直後です。つまり、ポートフォリオのウェイト更新はスイングトレード戦略よりもやや高い頻度でおこなわれることになります。これは理想的ではありません。より適切な更新間隔を見つける余地があるかもしれません。

いずれにしても、これらの更新頻度には注意しておいてください。次のステップではウィンドウ長やステップを変更するため、これらの頻度も変化します。それに対して、どのような影響が出るかを見ることで分析に有用な情報が得られます。

時間軸

時間軸は、おそらく実務取引におけるポートフォリオリバランスの中で最も重要なパラメータの一つです。なぜなら、RWECによるポートフォリオ安定性評価においても最も重要な要素だからです。直感的には次のように説明できます。時間軸が長いほど評価はより正確になりますが、その分リアルタイム性は低下し、現在の市場構造に対する感度は下がります。一方で時間軸が短い場合は、現在の市場構造をより強く反映できますが、その代わりノイズの影響を受けやすくなります。 

最適な時間軸というものは一意には存在しません。それは取引戦略の頻度や、スプレッドの平均回帰の半減時間に依存します。たとえばスプレッドの平均回帰が数時間から数日単位で起こる場合、20〜60バー程度の短い時間軸でも現在の関係性を十分に捉えられる可能性があります。一方で、平均回帰の半減時間が数週間から数か月に及ぶ場合は、ノイズに左右されないようにするために120〜250バー以上の長いウィンドウが必要になることがあります。

以下は、RWEC 504/90/22 (n_bars/window/step)でリバランスをおこなった際に得られた結果です。

図17:RWEC 504/90/22に基づくポートフォリオウェイトリバランスのバックテストレポート
図17:RWEC 504/90/22に基づくポートフォリオウェイトリバランスのバックテストレポート

これが結果として得られたバランス/エクイティのグラフです。

図18:RWEC 504/90/22によるポートフォリオリバランスのバックテスト結果(残高/エクイティ)

図18:RWEC 504/90/22によるポートフォリオリバランスのバックテスト結果(残高/エクイティ)

共和分検定ウィンドウの長さ

同じ時間軸とステップで、45日間の時間軸を設定した場合、どのような結果が得られるか見てみましょう。

def rolling_cointegration(self, data, window=45, step=22):
        """Compute rolling cointegration vectors"""

概して言えば、最適な時間軸を選択する際に伴うトレードオフについて上述したのと同様のことが、テストウィンドウの長さにも当てはまります。ただし、このパラメータは固有ベクトルの計算に直接関与するため、ポートフォリオのウェイトの値に直接影響を与えます。スコアリング時には、ポートフォリオウェイトの安定性の計算に直接影響します。一方で、ライブ取引において更新をおこなう際には、この影響はポートフォリオの回転率(ターンオーバー)として現れます。短いウィンドウを用いるとターンオーバーは高くなります。これはより適応的ではありますが、短期的なノイズも多く取り込んでしまいます。このノイズは、ウィンドウごとに推定される固有ベクトルの変動を大きくし、その変動はRWEC(コサイン距離)で測定されます。RWECの閾値に頻繁に達する場合、それは頻繁なリバランスによる高いポートフォリオターンオーバーを示唆します。

一方で、長いウィンドウを用いるとターンオーバーは低下し、壊れたペアを保有し続けるリスクが高まります。長いウィンドウでは、固有ベクトルの変化は遅く滑らかになりますが、その分、コインテグレーション関係が崩壊した際の対応が遅れることになります。

最も適切なウィンドウ長を見つける唯一の方法は、バックテストをおこなうことです。その際には、タイムフレームやバスケットの平均回帰の半減期も考慮する必要があります。バックテストの評価に用いる客観的な基準が、最適なウィンドウ長を見つけるための指針となります。ここでは評価基準として、相対ドローダウンを用います。以下は、RWEC 504/45/22 (n_bars/window/step)でリバランスをおこなった際に得られた結果です。

図19:RWEC 504/45/22に基づくポートフォリオウェイトリバランスのバックテストレポート
図19:RWEC 504/45/22に基づくポートフォリオウェイトリバランスのバックテストレポート

以下は、ウィンドウパラメータを半分にした場合の、結果として得られる残高/エクイティのグラフです。

図20:RWEC 504/45/22によるポートフォリオリバランスのバックテスト結果(残高/エクイティ)

図20:RWEC 504/45/22によるポートフォリオリバランスのバックテスト結果(残高/エクイティ)

重なり合うウィンドウの長さ

今回、90日間の期間設定は維持しつつ、重複する期間を1取引週に短縮します。

def rolling_cointegration(self, data, window=90, step=5):
        """Compute rolling cointegration vectors"""

このパラメータは、連続する固有ベクトル計算の間にウィンドウが進む時間期間(取引日数)の数を表します。ステップサイズは、信号の時間分解能と再評価の頻度を制御します。この頻度によって、新しいポートフォリオのウェイト(固有ベクトル)のセットが計算され、以前のセットと比較される頻度が決まります。 

1日といった小さなステップサイズを選択すると、高解像度の信号を扱うことになります。RWECの比較値はほぼ毎日更新されます。これは、長期的な関係の安定性を日々確認する手段となります。共和分関係の崩壊(RWECの大きな変化)は、それが起こった瞬間に検出できます。取引から撤退するか、バックテストで自動的におこなわれるリバランスを受け入れることで、ほぼ即座に対応できます。欠点としては、ステップサイズが非常に小さい場合、固有ベクトルの計算を比例して高い頻度でおこなう必要があります。これは大規模なポートフォリオにとっては問題となる可能性がありますが、通常は大規模なポートフォリオを扱わない平均的な個人投資家(本記事の焦点)にとっては、おそらく問題にはならないでしょう。

1取引月(約22日)のような大きなステップサイズは、比較的解像度の低いシグナルを意味します。ポートフォリオのウェイトとRWECシグナルは、月に一度再評価します。計算負荷は大幅に軽減されますが、信号の即時性は失われます。共和分関係が1日目に崩壊した場合、不安定性や再均衡の必要性を検知できるのは20日目以降になります。この遅延は、変化の激しい市場において大きな損失につながる可能性があります。

つまり、ステップサイズはポートフォリオのウェイトをリバランスする頻度と直接的な関係があるということです。ポートフォリオのウェイトを常に最新の市場データに基づいて決定し、ヘッジを可能な限り最適に近づけることも可能です。この場合、取引コスト(手数料、スリッページ)の上昇に対処する必要があり、統計的裁定取引で通常得られるわずかな利益が損なわれる可能性があります。あるいは、回転率を下げて取引コストを下げるという選択肢もあります。この工程の間、バスケット内のウェイトは古いままになります。市場が不安定な状況では、リスクを大幅に高めることになります。

図21:RWEC 504/90/5に基づくポートフォリオウェイトリバランスのバックテストレポート

図21 RWEC 504/90/5に基づくポートフォリオウェイトリバランスのバックテストレポート

図22:RWEC 504/90/5によるポートフォリオリバランスのバックテスト結果(残高/エクイティ)

図22:RWEC 504/90/5によるポートフォリオリバランスのバックテスト結果(残高/エクイティ)

以下は、異なるRWECウィンドウ長とステップにおける、同一時間軸での相対的水位低下率の比較表です。

n_bars ウィンドウの長さ 重なり合うステップ 結果として得られるデータポイント 相対的ドローダウン
504+90
90 22 23 18.18%
504+90
45 22 25 36.08%
504+90
90 5 101 85.11%

表1:異なるRWECウィンドウ長とステップにおけるバックテストの相対ドローダウン指標の比較

ここでの主な目的は、各RWECの主要パラメータがウェイトの再調整にどのように影響するかを示すことです。さまざまなバスケットを試していくうちに、最適なパラメータの組み合わせを見つける唯一の現実的な方法は、徹底的なテストをおこなうことだということがすぐにわかるでしょう。

しかし、この単純な比較からも、ウィンドウの長さを半分に短縮すると、相対的なドローダウンが18.08%から36.08%へと100%も悪化することが分かります。これは、最初のオプションが私たちのポートフォリオにより適していることを明確に示しています。

取引期間を1か月(22日間)から1週間(5日間)に短縮したところ、結果は18.8%から85.11%へと劇的に悪化しました。しかし、データポイントを23から101へと約5倍に増やしたところ、共和分関係に欠陥があることも判明しました。この最後の実行結果のグラフは、より大きなローリングウィンドウのステップでは検出されなかった構造的な変化を示しているように見えます。 

3つのRWECパラメータの組み合わせを用いたこれらの簡単な評価によって、それぞれのパラメータがバックテストに及ぼす影響を数値的および視覚的に示すことができ、また、構造変化の早期検出におけるオーバーラップウィンドウステップの役割を強化できることを願っています。このような共和分変化の検出は、次回の講演のテーマです。


結論

本記事では、共和分関係にある株式のバスケットのポートフォリオウェイトの更新をバックテストするための、考えられる方法の一つを紹介しました。CSV/TSVデータを汎用的なHashMapコレクションに読み込み、バックテストに合わせたタイムスタンプで順次読み込むことで、リアルタイムの取引更新をシミュレートできることを示しました。

新しいウェイトを計算するために使用される方法は、ローリングウィンドウ固有ベクトル比較法(RWEC)です。バックテスト結果における各パラメータの相対的な影響について簡単に説明しました。これらのパラメータとは、時間軸(バックテスト時間軸)、共和分ベクトルの計算時間軸(ウィンドウ長)、および重複ウィンドウ時間軸(フォワードステップ)です。これらの各パラメータについて、バックテストの結果を示し、その分析がライブトレードにおける最適なパラメータの選択にどのように役立つかを説明しました。

RWECを実行し、その結果を専用のデータベーステーブルに保存してCSV/TSV形式でエクスポートするためのPythonスクリプトと、テスターでデータを読み取るために必要なMQL関数を含むヘッダーファイルを提供します。

ファイル名 説明
 Experts\StatArb\CointNasdaq.mq5 サンプルEAのメインMQL5ファイル
 Include\StatArb\CointNasdaq.mqh サンプルEAのメインMQL5ヘッダファイル
 Include\StatArb\TestHelper.mqh サンプルEAのテストヘルパーヘッダファイル
 Files\StatArb\strategy_*.csv 記事で使用しているデータベースからエクスポートされたTSV形式のファイル
 rwec2db RWECを実行し、その結果を統合SQLiteデータベースに保存するPythonスクリプト 
 CointNasdaq.INTC.H4.20241213_20251213.000 記事で使用されているバックテスト設定

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

添付されたファイル |
MQL5でカスタムインジケーターを作成する(第2回):Canvasと針のメカニクスを使ったゲージ型RSIインジケーターの構築 MQL5でカスタムインジケーターを作成する(第2回):Canvasと針のメカニクスを使ったゲージ型RSIインジケーターの構築
本記事では、MQL5でゲージ型のRSIインジケーターを開発します。このインジケーターは、RSIの値を円形のスケール上の動く針で可視化し、買われすぎと売られすぎのレベルを色分けした範囲と、カスタマイズ可能な凡例を備えています。Canvasクラスを使用して、円弧、目盛り、扇形などの要素を描画し、新しいRSIデータに基づいて滑らかに更新されるようにします。
古典的な戦略を再構築する(第20回):現代のストキャスティクス 古典的な戦略を再構築する(第20回):現代のストキャスティクス
本記事では、古典的なテクニカル指標であるストキャスティクスを、従来の平均回帰ツールとしての使い方にとどまらず、どのように再解釈および再活用できるかを解説します。異なる分析視点からこの指標を捉え直すことで、慣れ親しんだ手法が新たな価値を生み出し、トレンドフォロー型の解釈を含む代替的な売買ルールの構築にも応用できることを示します。最終的に、MetaTrader 5ターミナルに搭載されているあらゆるテクニカル指標には未開拓の可能性が潜んでおり、試行錯誤を慎重に重ねることで、従来の見方では気づきにくい有意義な解釈を発見できることを示します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5でカスタムインジケーターを作成する(第1回):Canvasグラデーションを使用したピボットベースのトレンドインジケーターの構築 MQL5でカスタムインジケーターを作成する(第1回):Canvasグラデーションを使用したピボットベースのトレンドインジケーターの構築
本記事では、ユーザーが定義した期間にわたって高速ピボットラインと低速ピボットラインを計算し、これらのラインに対する価格の位置に基づいてトレンドの方向を検出し、矢印でトレンドの開始を知らせるとともに、必要に応じて現在のバーを超えてラインを延長するピボットベースのトレンドインジケーターを、MQL5で作成します。このインジケーターは、カスタマイズ可能な色で表示される個別の上昇線と下降線、トレンドの変化に応じて色が変わる点線の高速線、そしてトレンド領域の強調表示を強化するためのCanvasオブジェクトを使用した、線間のオプションのグラデーション塗りつぶしによる動的な可視化をサポートしています。