共和分株式による統計的裁定取引(最終回):特化型データベースを用いたデータ分析
はじめに
2025年初頭、私たちは一つの挑戦を受け入れました。それは、一般の個人トレーダー、すなわち一般的なノートPCと通常のインターネット接続、そして限られた運用資金だけを武器に市場へ挑む「孤高の戦士」のために、統計的裁定取引のフレームワークを開発することでした。それ以来、私たちは10本の記事を通じて、この研究と実験の過程、そして数学で市場に勝とうとした試みの成功と失敗について紹介してきました。
これらの記事はすべて、トレーダーの視点から統計的裁定取引をやさしく学べる入門書となることを目指して執筆しました。全体を通して、高校レベルの数学知識と基本的な開発スキルを持つ一般の方でも、統計的裁定取引戦略を理解し、自ら実装できるようになるための最低限の知識を提供することを目的としています。
連載の冒頭では、統計的裁定取引の基礎となる相関、共和分、定常性検定について解説しました。具体的には、エングル・グレンジャー共和分検定、ジョハンセン共和分検定、拡張ディッキー–フラー(ADF)検定、およびKwiatkowski-Phillips-Schmidt-Shin (KPSS)定常性検定を取り上げました。それぞれの目的や解釈、意味を理解することで、シリーズの初期段階からペアポートフォリオやバスケットポートフォリオを構築できるようになりました。また、statsmodelsのようなオープンソースの実務向けPython統計ライブラリを活用することで、これらの検定の数学的な詳細に深入りすることなく、実践的な活用方法に集中できました。
統計的裁定取引を語るうえで、データ分析とデータベースは欠かせません。MetaTrader 5にはSQLiteデータベースが標準で組み込まれているため、この点についても対応しました。第3部では、データベース環境の構築方法、初期スキーマの設計、さらにMQL5 Serviceによってデータベースを常に最新の状態へ保つ方法を説明しました。このServiceの導入は、データ分析を取引環境から切り離すための第一歩でもありました。そして本記事では、その分離を完成させます。
データベースと安定した共和分・定常性検定のパイプラインを整備した後、次に取り組むべき課題はポートフォリオの構築です。そのため、続く3本の記事では、共和分の強さ、ポートフォリオウェイトの安定性、平均回帰の半減期といった客観的な指標に基づく、最小限のスクリーニングおよびスコアリング手法を提案しました。スクリーニングで用いた仮説は、ナスダック上場の高流動性マイクロプロセッサ業界銘柄間における共和分でしたが、その原理は、他のあらゆる共和分ポートフォリオにも有効です。
さらに最後の3本の記事では、ライブ取引を監視するための代表的な手法について説明しました。具体的には、Rolling Windows Eigenvector Comparison (RWEC)とIn-Sample/Out-of-Sample ADF (IS/OOS ADF)を組み合わせて使用し、ポートフォリオウェイトのリバランスが必要かどうかを判断する方法を紹介しました。また、共和分関係に構造変化が生じたことを検出するために、Chow検定およびCumulative Sum of Squares (CUSUM)評価についても解説しました。
これら10本の記事でまとめた研究と実験は、統計的裁定取引の分野において、一般的な個人トレーダーが参入するための障壁が、この10年から15年の間に大きく低下したことを明らかにするうえで、非常に重要な役割を果たしました。取引速度、つまり総合的な注文執行速度に重点を置くのではなく、異なる市場や異なる時間軸の組み合わせに存在する、ほぼ無限とも言える銘柄間の予想外の関係性を発見することへ焦点を移すことで、私たちはこの挑戦を受け入れた際に抱いていた最初の仮説を改めて確認することができました。現在では、統計的裁定取引戦略は一般的なリテールトレーダーにも十分に実行可能なものとなっています。HFT(高頻度取引)分野で発生する法外なコストを回避できるため、2026年においても、個人トレーダーは統計的裁定取引戦略を運用するための魅力的な機会を見つけることができます。
私たちが説明してきたツールキットに唯一不足している要素は、SQLiteにかかるデータ分析の負荷を取り除くための、専門化されたデータベースです。これにより、私たちの愛用するMetatrader 5の組み込みデータベースは、本来設計された目的であるトランザクション処理に専念させることができます。エキスパートアドバイザー(EA)との連携、つまり売買エントリー/エグジットに関する「single source of truth」としての役割は、引き続きSQLiteに担わせるべきです。一方で、スクリーニング/スコアリングシステムやライブ取引モニタリングに必要なデータ分析処理は、専門的なシステムによって処理されるべきです。この統計的裁定取引をリテールトレーダー向けに紹介する一連の内容の最後のステップでは、私たちのデータ分析環境を、膨大なデータセットのリアルタイムモニタリングに対応できる状態へと整備します。さらに、この環境は将来的な運用規模の拡大にも対応できる、将来を見据えた構成となります。高価なハードウェアへの追加投資を必要とすることなく、運用規模の成長に対応できるためです。
統計的裁定取引における処理能力の関連性
あらゆる取引活動にはデータが必要です。最低限でも、売買注文を実行するためには、対象資産の現在価格が必要になります。通常は、それ以上のデータを利用します。少なくとも、価格が時間の経過とともにどのように変化してきたかを把握するためには、ある程度の価格履歴が有用です。やがて、私たちは価格の最大値・最小値、レンジ、平均値、ATR、出来高加重平均価格(VWAP)、そして価格履歴のさまざまな見方などを計算するようになります。より多くのデータを利用することで、価格が現在の値に到達した経緯をより深く理解できます。十分なデータがあれば、その資産価格が数時間後、数日後、あるいは数か月後にどの方向へ動く可能性があるかについて推測することも可能になります。資産価格データを利用してローソク足パターンを構築・分析したり、価格履歴を可視化しやすくするために、比較的単純なものから複雑なものまで、さまざまなインジケーターを作成したりすることもあります。単一の資産であっても、数十か月分の過去価格データを取得することは珍しくありません。
ペアトレードの場合、必要なデータ量は少なくとも2倍になります。スプレッドの平均値を継続的に計算する必要があり、さらに、そのペア関係が十分に強固で現在も維持されているかを検証するためには、より長期間の履歴データが必要になります。また、ボラティリティに基づいて動的な標準偏差の閾値を計算したい場合もあります。そのため、必要となるのは単により多くのデータだけではありません。リアルタイムで2つの銘柄のデータを読み込み、スプレッド、平均値、標準偏差による閾値、そしてボラティリティを計算するための、より大きな計算処理能力も必要になります。
ペアトレード戦略で必要とされるデータ量および計算能力は、通常、統計的裁定戦略で必要とされるものの一部に過ぎません。統計的裁定では、銘柄数に実質的な制限のないバスケットを対象として、共和分ベクトルおよび対応するポートフォリオウェイトを算出し、さらにポートフォリオウェイトの安定性を確認するためにローリングウィンドウ固有ベクトル比較を実行する必要があります。また、構造変化を事前に検知するために、CUSUMSQ (Cumulative Sum of Squares)を用いたChow検定も実施しなければなりません。これらすべての計算をリアルタイムで実行し、ライブ取引を監視する必要があります。最終的には、複数のバスケットに対して同時にこれらの処理を実行する必要があります。つまり、数十種類の銘柄の価格データを取得し、それらを可能な限り高速に処理して、アラート、構造変化、市場の混乱に適切なタイミングで対応できるようにする必要があります。
ジム・シモンズが1980年代後半に統計的裁定取引戦略の運用を開始した頃、大量のデータを日々処理するために必要な計算能力は非常に高価でした。ソフトウェアを開発する専門人材、ハードウェア設備、そして運用環境を維持するためのコストは、裕福な個人トレーダーであっても容易に負担できるものではありませんでした。当時、個人トレーダーにとっての最大の参入障壁は、高速光ファイバー回線やサーバーのコロケーションではありませんでした。そもそも、その時代にはHFTという概念自体がまだ存在していませんでした。
1980年代後半におけるリレーショナルデータベース管理システム(RDBMS)の推定コストは、Oracle、Ingres、IBMなどの製品で、永久ライセンスあたり約1万4千〜1万5千ドル程度でした。インフレ調整すると、現在の価値では約10万ドル程度に相当します。ただし、ここでは人件費やハードウェア費用は含めていません。これはあくまでソフトウェアライセンス費用のみのおおよその金額です。統計的裁定取引は、データ分析に必要な処理能力と、それを支える高価なツールのため、主に機関投資家のみが利用できる領域でした。しかし、この状況はその後数年間で劇的に変化しました。以降、その変化について見ていきます。
標準的なMetaQuotesのデモ口座では、約9,000種類の固有銘柄と、20種類以上の時間足を利用できます。

図1: MetaQuotesの標準デモ口座で利用可能な銘柄の数を示すスクリーンショット
統計的裁定取引は、資産価格間の関係性を発見することを目的としています。そのため理想的には、利用可能なすべての資産について、すべての組み合わせを、すべての時間足で検証したいところです。しかし、すべての可能な組み合わせを探索すると、組合せ爆発が発生します。そのため、実際にはスコアリングシステムに基づいて、分析対象となるペアやバスケットを選択します。
これまで使用してきたのはSQLiteのみです
利便性を考慮し、これまでデータベースとしてMetaTrader 5に組み込まれているSQLiteを使用してきました。しかし、誤解しないでください。利便性とともに提供されているのは、世界で最も利用されているデータベースの一つであり、優れた信頼性を持つソフトウェアです。実運用環境で十分に検証され、長年にわたって使用されてきた、非常に堅牢なシステムです。さらに、MetaTrader 5の一部として提供されているため、追加のインストールや設定を必要としません。また、プラットフォーム内でSQLiteを扱いやすくするために、SQLiteデータベース用のネイティブ関数セットがMQL5に用意されています。加えて、MetaEditor内には、MetaTrader5に組み込まれたSQLiteデータベースを検索・管理するためのネイティブなグラフィカルユーザーインターフェース(GUI)も提供されています。

図2:MetaEditorのGUIインターフェースからSQLiteへの接続を示すスクリーンショット
これらの特徴により、SQLiteは一般的な個人トレーダー向けの統計的裁定フレームワークを構築するうえで最適な選択肢となっています。SQLiteは信頼性と拡張性に優れ、標準で組み込まれているほか、MQL5のネイティブ関数やMetaEditorのGUIにも対応しています。そのため、MetaTrader 5環境で非常に扱いやすいデータベースです。しかし、SQLiteを手放すことなく、さらに優れた構成を実現することができます。つまり、SQLiteは本来設計された用途に活用し、データ分析については分析専用のデータベースを併用するという方法です。
SQLiteはOLTP(オンライントランザクション処理)システムです。これは、小規模なトランザクションを高速に処理することを目的として設計されたシステムです。SQLiteはこの用途において非常に優れた性能を発揮します。一方で、データ分析専用のデータベースとは、OLAPシステムを指します。OLAPは、通常は大規模なデータセットを対象として、履歴データに対する複雑で計算量の多い分析を効率的に実行するために設計されています。両者の用途には一部重なる部分もあります。たとえば、OLAPシステムを日々のトランザクションデータの保存・検索に利用することもできますし、本稿でこれまで行ってきたように、OLTPシステムを履歴データの分析に利用することも可能です。しかし、両者は本質的に異なる目的を持つシステムです。
こうした違いが明確に現れるのは、データ分析を本格的な規模で実行するようになってからです。これまで扱ってきた例では、分析対象は履歴データ全体のごく一部にすぎませんでした。ルックバック期間も、長くても2~3年程度に限定されていました。10年や20年分の履歴データを分析したことは一度もありません。また、共和分の検証対象となるバスケットも3~4銘柄程度に限定しており、それ以上の規模は扱っていません。さらに、スクリーニングおよびスコアリングシステムについても、半導体業界に対する当初の共和分仮説に基づき、単一の時間足(H4)のみを対象としていました。
しかし、たとえ一般的な個人トレーダーであっても、統計的裁定に本格的に取り組むのであれば、いずれは共和分以外のパターンを探すために、より長期間の履歴データを分析する必要に迫られるでしょう。また、5銘柄あるいは10銘柄から成るバスケットを、多数の組み合わせや複数の時間足で検証したくなるかもしれません。さらに、運賃指数や商品生産量といった非金融データを分析対象に加えることも考えられます。そして、そのような分析結果は、おそらくライブ取引の監視システムの基盤となるでしょう。このような状況を想像できるでしょうか。
分析対象となる銘柄数、時間足、履歴データの期間が増えていくにつれて、気が付けば自分自身が「ビッグデータ」をできるだけ高速に処理しなければならない状況に直面します。このような成長は、統計的裁定に取り組むうえではごく自然な流れです。データ処理の要求がこのように拡大していくことは十分に予想されるため、それに対応できる専用ツールの導入を検討する準備をしておくべきです。幸いなことに、2025年の現在では、「シャベルを買うために土地を売る」ような大きな投資をする必要はありません。かつては、高性能なOLAPシステムを利用するためには高価なソフトウェア・ライセンスを購入しなければなりませんでした。しかし現在では、高品質でありながら無料で利用でき、しかもオープンソースとして提供されているOLAPシステムを利用できます。それがDuckDBです。
DuckDB:無料で利用できるオープンソースのOLAPシステム
統計的裁定取引戦略は、HFTの一種、あるいは少なくとも高速通信や超低遅延の注文ルーティングに依存する手法である、という誤解がよく見られます。その結果、多くの人は、統計的裁定はスピードが絶対条件であるため、機関投資家だけが実践できる分野だと考えています。しかし、これは半分しか正しくありません。
確かに、多くの統計的裁定戦略は下位時間足(一般的には5分足未満)で運用されることを想定していますが、それは必須条件ではありません。相関関係や共和分、その他の統計的な関係は、より長い時間足でも十分に見つけることができます。スピードが重要であり、多くの戦略の中核を成していることは事実ですが、数年前まで機関投資家がこの分野を独占していた理由は、それだけではありませんでした。最大の理由は、データ分析に必要な計算能力のコストが非常に高く、その運用も複雑だったことです。
参考までに少し調べてみると、わずか15年前には、「個人トレーダーとヘッジファンドの間には、独自開発のOLAPハードウェアとソフトウェアによって築かれた『資本の堀』が存在していた」と表現されていました。2000年代半ばには、高額な導入費用だけでなく、技術面での参入障壁も非常に高かったのです。OLAPシステムでデータを分析するためには、ソフトウェア・ライセンスだけでも約10万~25万米ドルを投じる必要があり、さらに専用サーバーをラック単位で運用し、年間約20%の保守費用を支払わなければならないケースも珍しくありませんでした。技術的な障壁も同様に高いものでした。当時のOLAPシステムの多くは、多次元OLAPを採用していたため、あらかじめOLAPキューブを構築・前処理する必要がありました。また、それらの多次元集計データを操作・検索するには、MDXという専用言語を習得しなければなりませんでした。こうした企業向けシステムでは、後ほど紹介するように、慣れ親しんだSQLだけでCSVやParquetファイルを直接読み込んで分析するといった使い方はできませんでした。
しかし、この15年間で、個人トレーダーとヘッジファンドとの格差は大幅に縮まりました。依然としてスピードは重要であり、高頻度取引はなお個人トレーダーには現実的ではありませんが、現在ではフル機能を備えたOLAPシステムをノートPC上で動かすことができます。Mac、Windows、Linuxのいずれを利用していても、約50MB程度の実行ファイルをダウンロードするだけでDuckDBを利用でき、大規模なデータセットの分析をすぐに開始できます。
データ形式との高い互換性
DuckDBの最大の特徴の一つは、データ分析の現場で広く利用されているデータベース、データフレーム・エンジン、クラウドストレージ、各種ファイル形式と容易に連携できることです。この柔軟性は、拡張機能の仕組みによって実現されています。DuckDBクライアントをインストールすると、利用可能な拡張機能を確認できます。DuckDBの拡張機能は、コア開発チームによる公式提供に加え、コミュニティからの貢献によって継続的に拡充されています。これらの拡張機能により、DuckDBは次のようなデータソースとの読み書きに対応しています。
- Pandas、Apache Arrow、NumPy、Polarsなどのデータフレームエンジン
- MySQL、Postgres、SQLiteなどのデータベース
- Amazon S3、Google Cloud Storage、Azure Blob Storageなどのクラウドストレージサービス
- CSV、JSON、Parquet、およびIcebergファイル形式
本稿執筆時点において、このレベルの柔軟性を備えたオープンソースのシステムは、他にはほとんど見当たりません。
運用のシンプルさ
DuckDBは、SQLiteと同様のシンプルな運用性をデータ分析パイプラインにもたらします。SQLiteと同じく、DuckDBもインプロセス、管理不要、組み込み型のデータベースです。そのため、高い分析性能を提供するだけでなく、異なるデータソースやファイル形式間の相互運用性も大幅に向上します。複数のソースからデータを容易に統合できるほか、ローカル環境とクラウド環境との間でデータを移動することも簡単です。
ここで強調しておきたいのは、本記事ではSQLiteをDuckDBに置き換えることを提案しているわけではないという点です。私たちは、分析処理という明確な目的のために、既存のシステムへDuckDBを追加するだけです。SQLiteは、トランザクション処理を重視した汎用リレーショナルデータベースである一方、DuckDBは分析処理に特化したOLAPツールです。そのため、EAとのインターフェースは従来どおりであり、組み込みのSQLiteとMQL5のネイティブライブラリを引き続き利用します。
OLAPとOLTPの実践的な違い
SQLite OLTPシステムとDuckDB OLAPシステムの違いをより理解するために、実際のトレードデータを想定した異なる規模のデータに対して両者を動作させてみましょう。そのために、ここでは非常にシンプルな3ステップのPythonスクリプトを使用します。このスクリプトでは、いくつかの株式ティッカー(銘柄)の合成データを生成し、そのデータに対して「銘柄ごとの平均価格」を計算するという一般的な集計処理を実行します。これにより、両システムのアーキテクチャ上の違いが実際の動作としてどのように現れるかを確認できます。
DuckDBは複数のプログラミング言語向けのDuckDBクライアントパッケージを提供しています。Pythonで利用する場合は、以下のコマンドを実行するだけです。
pip install duckdb
DuckDBには外部ライブラリへの依存はほとんどありません。例外としてWindows環境では、Microsoft Visual C++再頒布可能パッケージが必要になります。
そうすれば、他のPythonパッケージと同じように、スクリプトにインポートするだけで済みます。
import sqlite3 import duckdb import pandas as pd import numpy as np import time
SQLiteとDuckDBのデータ分析性能の違いを示すために、ここでは合成データを用いたシンプルなベンチマークを実施します。まず、8種類のティッカーについて、価格と出来高から成る100万行の合成データを生成します。この例では、2023年の最初の1秒から連続する100万件のティックデータを生成しています。これは約11.5日分の取引データに相当し、およそ半月分のデータ量です。ただし、実際の運用環境では、分析対象となるデータはこれより何桁も大きな規模になることが一般的です。
def generate_data(filename="finance_data.csv"): print("Generating synthetic data...") tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'NVDA', 'META', 'NFLX'] n_rows = 1_000_000 df = pd.DataFrame({ 'timestamp': pd.date_range(start='2023-01-01', periods=n_rows, freq='s'), 'ticker': np.random.choice(tickers, n_rows), 'price': np.random.uniform(100, 500, n_rows), 'volume': np.random.randint(1, 1000, n_rows) }) df.to_csv(filename, index=False) print(f"Created {filename} with {n_rows} rows.")
これにより、以下のような100万行のデータフレームが生成されます。
| timestamp | ticker | price | volume |
|---|---|---|---|
| 2023-01-01 00:00:00 | TSLA | 432.15 | 842 |
| 2023-01-01 00:00:01 | AAPL | 120.50 | 12 |
| 2023-01-01 00:00:02 | MSFT | 315.88 | 500 |
表1:SQLiteとDuckDBのベンチマーク用に生成した合成表形式データのサンプル
この表形式データをCSVファイルとして保存した後、インメモリのSQLiteに対して、各ティッカー(銘柄)ごとの平均価格を計算させます。
def benchmark_sqlite(filename): conn = sqlite3.connect(":memory:") # Using memory for a fair speed test cursor = conn.cursor() # Load data df = pd.read_csv(filename) df.to_sql("prices", conn, index=False) start_time = time.perf_counter() query = "SELECT ticker, AVG(price) FROM prices GROUP BY ticker" cursor.execute(query) results = cursor.fetchall() end_time = time.perf_counter() conn.close() return end_time - start_time
同じCSVファイルをDuckDBに渡して同じ計算を実行しますが、今回はDuckDBがCSVを直接クエリできるため、データをロードする必要はありません。
def benchmark_duckdb(filename): conn = duckdb.connect(database=':memory:') start_time = time.perf_counter() query = f"SELECT ticker, AVG(price) FROM '{filename}' GROUP BY ticker" results = conn.execute(query).fetchall() end_time = time.perf_counter() return end_time - start_time if __name__ == "__main__": csv_file = "finance_data.csv" generate_data(csv_file) print("\nStarting Benchmarks...") sqlite_time = benchmark_sqlite(csv_file) print(f"SQLite execution time: {sqlite_time:.4f} seconds") duckdb_time = benchmark_duckdb(csv_file) print(f"DuckDB execution time: {duckdb_time:.4f} seconds") speedup = sqlite_time / duckdb_time print(f"\nDuckDB was {speedup:.1f}x faster than SQLite for this query.")
このスクリプトはbench.pyという名前で記事に添付されています。これを100万行分実行すると、約50MBのCSVファイルが生成されるはずです。2コアしか搭載していない低スペックのノートPCで実行したところ、SQLiteの実行時間とDuckDBの実行時間の間に、以下のような関係が得られました。
Generating synthetic data... Created finance_data.csv with 1000000 rows. Starting Benchmarks... SQLite execution time: 1.3408 seconds DuckDB execution time: 1.3442 seconds DuckDB was 1.0x faster than SQLite for this query.
このデータ規模では、両方のシステムは同等でした。同じスクリプトを1000万行分実行して約500MBのCSVファイルを生成することで、データ量を10倍に拡大すると、違いが見え始めました。
Generating synthetic data... Created finance_data.csv with 10000000 rows. Starting Benchmarks... SQLite execution time: 18.0329 seconds DuckDB execution time: 9.0836 seconds DuckDB was 2.0x faster than SQLite for this query.
ここでは偶然にも丸めたような数字になっていますが、これはあくまで偶然です。確実に言えることは、このスクリプトを何度も実行し、3台の異なるマシンで試した場合、ここに示した値とは異なる絶対的な実行時間になるということです。当然ながら、これらの結果は使用するマシンのスペック、特にCPUコア数に大きく依存します。そのため、絶対値には注目しないでください。注目すべきなのは相対的な値、つまりデータ量を変化させたときのSQLiteとDuckDBの実行時間の比率です。ぜひ1億行に変更して試してみてください。
ここで起きていることは、SQLiteには起動時間がほぼゼロであるという点です。100万行の処理を要求すると、すぐに処理を開始します。一方、DuckDBにはクエリオプティマイザとベクトル化実行エンジンがあり、これらの処理には数ミリ秒(このような2コアの低スペックマシンでは数秒)かかります。また、列指向ストレージシステムであるDuckDBは、平均価格を計算するために必要な列だけ、つまりprice列だけを自然に読み込みます。一方、行指向システムであるSQLiteは、timestamp、ticker、price、volumeを含む各行全体を読み込みます。DuckDBでは、これら不要な列はディスク上で物理的に無視されます。そのため、100万行から1000万行へデータ量を増やすと、DuckDBが読み込むデータ量はSQLiteよりも大幅に少なくなります。。
最後に、SQLiteは1行ずつ処理し、単一クエリでは通常シングルスレッドで動作します。一方、DuckDBは、いわゆるベクトル化実行を使用します。これはデータをバッチ単位でCPUへ送り、同時に利用可能なすべてのコアを使用して完全な並列処理を行う方式です。繰り返しになりますが、データ量が増加するほど、この2つのシステム間の性能差も大きくなります。公平な比較としてSQLiteが勝つケースを見るなら、1000万行の中から特定の取引IDを検索するようなクエリを実行してみるとよいでしょう。その場合、DuckDBを圧倒するでしょう。なぜなら、それこそがSQLiteの得意分野であり、SQLiteが設計された目的だからです。
ここではCSVファイルを使用している点にも注意が必要です。しかし、DuckDBがParquetファイルや独自のネイティブ形式である.duckdbフォーマットをクエリできる能力を利用すれば、性能差はさらに広がります。ただし、データ分析性能や全体的な速度の向上は、統計的裁定取引システムに特化したOLAPシステムを導入することで得られるメリットの一部にすぎません。もう1つの大きなメリットは、ネイティブの時系列結合です。
ASOF JOIN
統計的裁定取引では、時間が主キーです。これは比喩ではありません。文字通りの意味です。本連載で構築してきたSQLiteデータベースのスキーマを見ると、ほとんどのテーブルが主キーとしてタイムスタンプを持っていることに気づくでしょう。データの大部分が時系列データであるため、これは自然な選択です。つまり、タイムスタンプは統計的裁定取引だけでなく、取引全体においても、ドメイン固有の自然キーのような役割を果たします。
この事実を踏まえて、以下の2つのテーブルを使って次の質問に答える必要があるとします。「銘柄Bが取引された時点で、銘柄Aの価格はいくらだったか?」銘柄Aの価格はmarket_dataテーブルに保存されており、銘柄Bの取引タイムスタンプはtradeテーブルに存在します。そのため、これら2つのテーブルを結合する必要があります。

図3:schema-0.5のmarket_dataテーブルとtradeテーブルを示す図
ご存じの通り、SQLのJOIN句では、対象となるテーブル間で一致するフィールドが必要です。そうでなければ、結果として0行が返されます。この場合、探している一致フィールドはタイムスタンプです。クエリは次のような形になります。
SELECT t.ticket, t.side, t.price AS trade_price, m.price_open, m.price_high, m.price_low, m.price_close, m.timeframe FROM trade t JOIN market_data m ON t.tstamp = m.tstamp AND t.symbol_id = m.symbol_id WHERE t.tstamp = 1708531200 -- The timestamp of our reference trade AND m.timeframe = 'M1'; -- We need the timeframe because of the market_data composite primary key
このクエリが機能するためには、market_dataのタイムスタンプ(m.tstamp)と取引のタイムスタンプ(t.tstamp)が、まったく同じ秒で一致している必要があります。上記の例の場合、2024年2月21日16:00:00 UTCのクオートが必要になります。正確に16:00:00 UTCでなければなりません。すでにお気づきかもしれませんが、取引とクオートがまったく同じ秒に発生することを期待するのは現実的ではありません。もし私たちのシステムがこのようなクエリに依存していたら、非常に脆弱なトレーディングシステムになってしまいます。なぜなら、ほとんど、あるいはすべてのクエリが失敗する可能性があるからです。さらに重要なのは、実際には私たちが知りたい情報はこれではありません。実際に知りたいのは、銘柄Bが取引された時点での、銘柄Aの直近のクオートです。
これは、サブクエリを使用する方法、またはLIMIT句を利用して、取引時刻以前、あるいは取引時刻と同時刻に存在する最も新しいクオートを取得する方法で解決できます。
SELECT t.*, m.* FROM trade t JOIN market_data m ON m.symbol_id = t.symbol_id WHERE t.tstamp = 1708531200 AND m.timeframe = 'M1' AND m.tstamp <= t.tstamp ORDER BY m.tstamp DESC LIMIT 1;
この標準SQLの方法は、SQLiteを含むほぼすべてのデータベースで利用できるという利点があります。しかし、いくつかの欠点もあります。特に重要なのは、やはりパフォーマンスです。最も近い価格を取得するために、「取引時刻以下のタイムスタンプの中で最大のものを探す」という処理を行う場合、データベースに対して、ある銘柄のすべての過去レコードを手動でソートし、その中から最新の1件を選択するよう指示しています。この処理は非常にコストが高くなります。1件の取引ごとに、データベースは検索処理とソート処理を実行する必要があります。たとえば、1,000件の取引と1,000,000件のクオートを結合する場合、これはデータ分析では一般的なシナリオですが、処理は非常に遅くなる可能性があります。
DuckDBやその他の専門的なOLAPシステムでは、この問題をASOF JOINと呼ばれる機能によって解決します。これは、時系列分析のために設計された時系列結合であり、列指向データベースにおいて、タイムスタンプが完全には一致しないテーブル同士を結合することを可能にします。標準的なSQLのサブクエリを使って時系列結合を行う場合と、DuckDBの専用機能であるASOF JOINを使用する場合の違いを示すために、両者のパフォーマンスを比較する別のシンプルなベンチマークスクリプトを用意しました。このスクリプトでは、以下の処理の性能を比較します。
サブクエリとLIMIT句を使用したSQLiteクエリ
sql_query = """
SELECT SUM(t.quantity * (
SELECT q.bid_price FROM q
WHERE q.ticker = t.ticker AND q.timestamp <= t.timestamp
ORDER BY q.timestamp DESC LIMIT 1
)) FROM t
""" ASOF JOINを使用したDuckDBクエリ
duck_query = """
SELECT SUM(t.quantity * q.bid_price)
FROM t ASOF JOIN q ON t.ticker = q.ticker AND t.timestamp >= q.timestamp
""" このスクリプトはbench_asof.pyという名前で添付されています。ターゲットとするハードウェアである低スペックのマシンで実行すると、これと似たような結果が得られるはずです。
--- Generating 1,000,000 trades and 100,000 quotes --- --- Running Engine-Only Benchmark (Computing SUM) --- DuckDB Time: 1.1637s SQLite Time: 2.1329s [WINNER]: DuckDB is 1.8x faster at this scale.
繰り返しになりますが、データ量を10倍に増やしてスケールさせると、その差もさらに大きくなります。ここで注意していただきたいのは、私は意図的にこのベンチマークを低スペックな2コアマシンで実行しているという点です。この選択には明確な目的があります。それは、平均的な個人トレーダー向けにこの統計的裁定取引フレームワークを開発するという、本連載の主な目標に合わせるためです。しかし同時に、この選択によってDuckDBが本来持つ並列処理能力は制限されています。DuckDBはデフォルトで利用可能なすべてのCPUコアを使用するためです。したがって、最新のデスクトップPCやノートPCで同じ分析を実行すれば、DuckDBの実行時間の数値はここで示したものよりも大幅に低下することがわかるでしょう。
--- Generating 10,000,000 trades and 1,000,000 quotes --- --- Running Engine-Only Benchmark (Computing SUM) --- DuckDB Time: 8.6416s SQLite Time: 22.2207s [WINNER]: DuckDB is 2.6x faster at this scale.
ASOF JOINが利用できることは、統計的裁定取引の分野で競争する個人トレーダーにとって、ゲームチェンジャーとなります。上で示した例やベンチマークでは、非常に単純なケースを扱っています。しかし、クエリにさらに多くの機能、例えばアスク価格、出来高、ボラティリティなどを追加していくと、DuckDBが提供するASOF JOINを使わない標準SQLによるクエリは、デバッグや保守が難しくなり、パフォーマンスもさらに悪化します。なぜなら、1件の取引ごとに、データベースはクオートテーブルに対して個別の検索処理を実行する必要があるからです。
一方、OLAPシステムでは時系列結合を主要な機能として扱い、この「ある時点での値」を取得するロジック専用に設計されたアルゴリズムを使用します。その結果、クエリはシンプルで、計算効率も高くなります。DuckDBはネストループ結合をおこないません。ここで、先ほど説明したベクトル化実行が活用されます。DuckDBはデータを「チャンク」単位でCPUキャッシュに送り込み、標準的なノートPCでも数百万行規模の結合を数ミリ秒で実行できます。また、DuckDBは列指向データベースであるため、データセット全体ではなく、必要な列だけ、つまりタイムスタンプ、銘柄、価格の列だけを読み込みます。
このベクトル化実行の能力が特に発揮されるのが、ポートフォリオのリバランシングで使用するRWECの計算です。これは、本連載第8回で説明したものです。
RWEC
DuckDBには組み込みの配列関数があり、データベースが大きくなるにつれて、RWEC計算のパフォーマンスを大幅に向上させることができます。以前使用したPythonによる方法では、連続するベクトル間のコサイン類似度を以下のように計算していました。
def vector_similarity(self, vectors_df): """Compute cosine similarity between consecutive vectors""" similarities = [] for i in range(1, len(vectors_df)): vec1 = vectors_df.iloc[i-1].values vec2 = vectors_df.iloc[i].values cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) angle_deg = np.degrees(np.arccos(np.clip(cos_sim, -1, 1))) similarities.append({ 'date': vectors_df.index[i], 'cosine_similarity': cos_sim, 'angle_degrees': angle_deg, 'stable': angle_deg < 30 # Threshold }) return pd.DataFrame(similarities).set_index('date')
このPythonのロジックには、DuckDBの配列関数の中に直接置き換え可能な機能があります。それがarray_cosine_similarity(v1, v2)です。ここで注意すべき点は、コサイン類似度を計算する際、2つのベクトルの内積を、それぞれのベクトルの大きさ(ノルム)の積で割っているということです。これはコサイン類似度の数学的な定義そのものです。DuckDBで私たちのvector_similarityメソッドを再現するには、ウィンドウ関数LAGを使用して、現在のベクトルと直前のベクトルを比較します。これにより、DuckDBはこれらの配列演算をSIMD(単一命令複数データ)命令を利用して処理できます。その結果、過去のデータや取引履歴のリポジトリが大規模データセットへと成長していくにつれて、Pythonのループ処理よりもはるかに高速に処理できるようになります。
また、DuckDBがアウトオブコア実行に対応している点も考慮する必要があります。つまり、利用可能なRAM容量を超えるデータであっても処理できます。一方で、現在のpandasを使ったアプローチはメモリ容量によって制限されます。行単位で処理するPythonのイテレーションと、OLAPシステムのベクトル化エンジンとの違いを見るために、3つ目のベンチマークを実行できます。ここでは、添付したrwec.pyスクリプトで使用しているネイティブなPython/NumPyループと、DuckDBのarray_cosine_similarity関数を比較します。
import duckdb import pandas as pd import numpy as np import time def generate_eigen_dataset(n_rows=1_000_000, vec_dim=2): """Generates a large set of synthetic eigenvectors for testing.""" print(f"--- Generating {n_rows:,} eigenvectors (dim={vec_dim}) ---") # Generate random vectors and normalize them data = np.random.randn(n_rows, vec_dim) norms = np.linalg.norm(data, axis=1, keepdims=True) normalized_vecs = data / norms df = pd.DataFrame({ 'date': pd.date_range(start='2000-01-01', periods=n_rows, freq='h'), 'vec': list(normalized_vecs) }) return df
生成されるベクトルは、必ず正規化することを忘れないでください。これは、ジョハンセン共和分検定をおこなう場合と同じ考え方です。RWECの計算では、この正規化は必須のステップです。なぜなら、コサイン類似度の計算式は2つのベクトル間の角度を測定するものであり、その計算結果はベクトルの大きさ(ノルム)が正規化されていることを前提としているためです。
統計的裁定取引において、共和分ベクトルは資産間のヘッジ比率を表します。たとえば、固有ベクトル[1, -2]は、[10, -20]とまったく同じ関係性を表しています。どちらも1:2の比率を示しているためです。もしローリングウィンドウによる計算で、ある期間では[1, -2] が得られ、次の期間では[10, -20]が得られたとしても、実際には関係性は変化していません。正規化を行うことで、ベンチマークではJohansen検定の出力に含まれる任意のスケールの違いではなく、ベクトルの方向、つまりヘッジ比率そのものを評価できます。
def benchmark_rwec(df): # --- SETUP DUCKDB --- con = duckdb.connect(":memory:") # Register the dataframe as a virtual table con.register("eigen_table", df) print("\n--- Running RWEC Benchmark (Cosine Similarity + Angle) ---") # 1. DuckDB Benchmark (Vectorized SQL) # Uses array_cosine_similarity and LAG to compare consecutive rows duck_query = """ SELECT AVG(DEGREES(ACOS(inner_sim))) FROM ( SELECT LEAST(GREATEST(array_cosine_similarity( vec::DOUBLE[2], LAG(vec::DOUBLE[2]) OVER (ORDER BY date) ), -1), 1) as inner_sim FROM eigen_table ) WHERE inner_sim IS NOT NULL """ start = time.perf_counter() duck_res = con.execute(duck_query).fetchone()[0] duck_time = time.perf_counter() - start print(f"DuckDB Time: {duck_time:.4f}s (Result Avg Angle: {duck_res:.2f}°)") # 2. Python/NumPy Loop Benchmark # We iterate through the dataframe as in the rwec.py script start = time.perf_counter() similarities = [] # Replicating the logic from rwec.py vector_similarity() vecs = np.stack(df['vec'].values) for i in range(1, len(vecs)): vec1 = vecs[i-1] vec2 = vecs[i] # Manual cosine similarity calculation cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) angle_deg = np.degrees(np.arccos(np.clip(cos_sim, -1, 1))) similarities.append(angle_deg) py_res = np.mean(similarities) py_time = time.perf_counter() - start print(f"Python Loop Time: {py_time:.4f}s (Result Avg Angle: {py_res:.2f}°)") print(f"\n[WINNER]: DuckDB is {py_time/duck_time:.1f}x faster for RWEC logic.") if __name__ == "__main__": # Test with 500,000 rows to see the gap eigen_df = generate_eigen_dataset(n_rows=500_000) benchmark_rwec(eigen_df)
このスクリプトはbench_rwec.pyという名前で添付されています。低スペックのマシンで実行すると、以下のような結果が得られるはずです。
--- Generating 500,000 eigenvectors (dim=2) --- --- Running RWEC Benchmark (Cosine Similarity + Angle) --- DuckDB Time: 0.9603s (Result Avg Angle: 90.01°) Python Loop Time: 21.9560s (Result Avg Angle: 90.01°) [WINNER]: DuckDB is 22.9x faster for RWEC logic.
先ほど確認した2つのベンチマークとは異なり、このケースでは入力データを50万個の2次元固有ベクトルまで増やした段階でも、最初から非常に大きな性能差が発生しています。array_cosine_similarity関数は、Pythonで使用していた手動のnp.dot計算と正規化処理の両方を置き換えます。さらに、Pythonループ(for i in range)を排除することで、各行を処理するたびにPythonインタープリタと内部のCライブラリ間を行き来することで発生していたオーバーヘッドを削減できます。また、前述したように、DuckDBはこれらの配列処理をチャンク単位で実行します。つまり、1回につき1つの角度を計算しているわけではありません。複数のデータをまとめたバッチ単位で並列処理するため、以前使用していたPython/SQLiteアルゴリズムと比較すると、このような大幅な性能向上が得られます。
上記の方法は、2次元の固有ベクトル、つまり2つの銘柄を扱う場合に機能します。3つ以上の銘柄で構成されるバスケットを分析したい場合は、次元をベクトル変数として渡すだけで対応できます。::DOUBLE[n]キャストのnは、共和分ベクトルに含まれる実際のアセット数と一致している必要があります。Pythonのf-stringを使用すれば、次元(vec_dim)をクエリ内に直接埋め込むことができます。
# vec_dim is the number of assets (2 for a pair) vec_dim = len(df['vec'].iloc[0]) duck_query = f""" SELECT AVG(DEGREES(ACOS(inner_sim))) FROM ( SELECT LEAST(GREATEST(array_cosine_similarity( vec::DOUBLE[{vec_dim}], LAG(vec::DOUBLE[{vec_dim}]) OVER (ORDER BY date) ), -1), 1) as inner_sim FROM eigen_table ) WHERE inner_sim IS NOT NULL """
これは重要な点です。なぜなら、DuckDBのarray_cosine_similarityは、入力配列の長さが固定されていることを要求するためです。DOUBLE[2]とDOUBLE[3]を比較しようとすると、エンジンはバインダーエラーを発生させます。
結論
共和分関係にある株式を通じた統計的裁定取引に関する入門シリーズの最終回となる本記事では、データ分析パイプラインに特化型データベースを組み込むことを提案しました。具体的には、無料で利用でき、オープンソースでありながら、金融機関レベルの性能を備えたOLAPシステムであるDuckDBの利用を推奨しています。
数年前までは、統計的裁定取引分野における個人トレーダーにとって最大の参入障壁は、売買執行速度ではなく、リアルタイムデータ分析に必要な計算能力であったことを指摘しました。この点を裏付けるために、2010年頃に同様のシステムを構築する場合に必要だったコストの推定値を示し、さらに、その実装に伴う技術的な複雑さについて簡単に説明しました。
最後に、OLAPシステムを導入することで、リアルタイムデータ分析のステップがどのように改善されるかを示すため、システム内で頻繁に使用される以下の3種類の計算について、DuckDBとSQLiteの性能比較ベンチマークを実施しました。具体的には、平均価格計算のための単純な集計処理、過去のクォート比較のための2テーブル結合処理、Pythonのネストされたループではなく、DuckDBのネイティブ関数を使用したRWEC計算です。
低スペックのマシン上で実施したベンチマーク結果では、DuckDBによる計算はSQLiteと比較して2倍から23倍高速でした。また、サンプルデータ量を増やすほど、その性能差はさらに拡大しました。この結果は、私たちがデータ分析パイプラインにこの特化型システムを組み込むことを推奨する主な理由を裏付けています。つまり、将来的なデータ量の増加に対応でき、大規模データセットに対しても長期的に利用可能なシステム基盤を構築するためです。
| ファイル名 | 説明 |
|---|---|
| bench.py | SQLiteとDuckDBの単純集計処理を比較するベンチマークを実行するPythonスクリプト |
| bench_asof.py | SQLiteの標準SQLによる時系列結合とDuckDBのASOF JOINを比較するベンチマーク用Pythonスクリプト |
| bench_rwec.py | RWEC計算において、NumPyのネストされたループ処理とDuckDBのコサイン類似度関数を比較するベンチマーク用Pythonスクリプト |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21507
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
ラリー・ウィリアムズの『市場の秘密』(第13回):隠れスマッシュデー反転パターンの自動化
プライスアクション分析ツールキットの開発(第62回):MQL5による適応型平行チャネル検出とブレイクアウトシステムの構築
MQL5取引ツールのアクセシビリティ課題を克服する(第2回):Pythonのテキスト読み上げエンジンでEAを音声対応にする
MQL5取引ツール(第21回):回帰グラフにサイバーパンクテーマを追加する
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索