MetaTrader 5における低頻度定量戦略(第1回):OLAP対応データストアの構築
はじめに
私たちはちょうど、一般的なノートPC、通常のインターネット接続、そして限られた資金だけを備えた平均的な個人トレーダー向けに、統計的アービトラージの基本概念を紹介する一連の記事を終えたところです。これを可能にするために、私たちは低頻度の平均回帰戦略に焦点を置いています。これは、高頻度取引(HFT)が依然として多額の資本を必要とする分野であり、通常は機関投資家に限定されていると仮定しているためです。必要なデータさえ十分に保有していれば、個人トレーダーでも低頻度の定量取引で成功できることを示せたと考えています。なぜなら、必要となる計算能力はすでに広く利用可能になっているからです。十分なデータとは、ニッチな機会を発見するために必要なデータを意味します。十分な計算能力とは、必要な時間内にデータを処理できるハードウェアおよびソフトウェアを意味します。
連載最後の記事では、データ分析のために専用データベースを使用することを提案しました。これは、私たちのパイプライン内でSQLiteと併用する、オープンソースかつ無料のデータベースです。この提案の主な考えは、データセットが発展し成長するにつれて、SQLiteのようなオンライントランザクション処理(OLTP)システムは、入門的な概念の適用例を示していた段階では適していたかもしれませんが、データ分析の作業に最適なツールではなくなるということです。言い換えると、私たちが実際のトレードシステムへ進んでいくにつれて、専用のデータ分析ツールが必要になります。その最初のもの、そしておそらく最も重要なものが、専用のOLAPデータベースです。
さて、同じ物語の新しい章と考えることができるこの新しい連載では、このOLAP対応のデータ分析システムの実装を開始する時が来ました。しかし、もし前回のシリーズを追いかけ、ツールを実行し、提供されたスクリプトや統計手法を試していたなら、SQLiteデータベース内にすでに大量のデータが保存されているかもしれません。そして、そのデータの一部は、共和分テストやスコアリングシステムの出力結果のような、以前のデータ分析結果である可能性があります。新しいシステムへ移行または変換する必要があるこのデータの一部を保持したいと思うかもしれません。しかし、たとえこのデータが重要ではないと思ったとしても、将来のある時点で、SQLiteを含む他のデータベースシステム(Postgres、MySQLなど)からデータをインポートする必要に直面する可能性があります。したがって、この新しい章での最初の課題は、この移行がどれほど高速で単純に実行できるかを示すことです。
レガシーデータまたは補完的なデータを新しいシステムへ移行した後、次のステップは、新しいデータを直接新しいシステムへ取り込み、欠損や重複なしに、最新状態を維持しながら不要なデータを削除・整理することです。このために、業界レベルのファイル形式標準であるParquetと、業界レベルのファイルシステムパーティション方式であるHiveを使用します。これにより、データはローカルシステムとほとんどのクラウドプロバイダー間でポータブルな状態を維持し、高い性能を持ち、簡単にクエリできるようになります。
このOLAP対応のセットアップが確立されると、データ分析を開始できます。目的は、取引アイデアを評価し、ニッチなパターンを探し、よく知られた取引セットアップをバックテストして、それらが高機能なエキスパートアドバイザー(EA)開発に適しているかどうかを判断することです。つまり、私たちのデータ分析は時間を節約するものであるべきです。新しいアイデアを数秒でフィルタリングできるようにし、高性能なMQL5エキスパートアドバイザー(EA)を開発してバックテストするために何時間、あるいは何日も費やす代わりとなります。複数のデータソース(ブローカーや商用データプロバイダー)からデータを結合できることで、貨物料金やマクロ経済集計データセットのような金融以外のデータを含む、通常とは異なる組み合わせを探索できます。
注意すべき点が一つあります。HFTは通常、ミリ秒、あるいはマイクロ秒単位の取引と関連付けられています。しかし、ここでの目的においては、数秒未満の頻度は対象外です。本連載の記事では、あらゆる時間足を対象としたニッチ戦略の開発に関心がありますが、数秒未満のものは決して扱いません。勝算がほぼゼロである競争には参加したくありません。その代わりに、小規模であることを自分たちの優位性として利用する方法を探ります。速度は確かに重要です。しかし、重要なのは注文ルーティング速度や約定速度だけではありません。機会発見の速度を高めることによっても、取引上の優位性を得ることができます。もし優位性を発見できれば、ミリ秒を奪い合う必要なく、ブルーオーシャンの領域で、その優位性を活かすことができます。
ここで説明する原則(パーティション、ファイルシステムレイアウト、ゼロコピーデータ読み取り、そしてデータベースネイティブ関数の利用)は、あらゆるOLAPシステムに適用できることを覚えておいてください。利便性のため、本連載では前回の記事で説明したものと同じ、無料かつオープンソースのシステムであるDuckDBを使用します。もしDuckDBに馴染みがない場合は、この選択の理由をよりよく理解するために、その記事を確認することをお勧めします。
SQLiteデータベースをParquetファイルに変換する
個人トレーダー向けの統計的アービトラージフレームワークを開発するための研究結果を紹介してきた中で、私たちはMetaTrader 5に組み込まれているSQLiteを使用して、最小限のデータベーススキーマを構築しました。このスキーマは、私たちの知識が向上し、新しい要件が発生するにつれて、自然に発展していきました。その最新バージョン(statarb-0.5)では、以下のような構成になっていました。

図1:statarb-0.5 SQLiteデータベースのエンティティリレーションシップ図
このスキーマは実際に効果的に実装され、私たちは実験やバックテストでこのデータベースを使用しました。このデータベースには、市場データだけでなく、多くの統計テストやスコアリングシステムによる実験結果も保存していました。これらの概念をどのように試してみたかによって、このデータは価値を持つ可能性があります。また、そのサイズも大きくなる可能性があります。たとえ、このデータを学習目的だけに役立つ使い捨てのデータと考えたとしても、ある時点での運用において、すでにOLTPシステムに保存されているデータを再利用する必要が発生する可能性を考慮すべきです。たとえば、PostgresやMySQLのようなデータベースに、すでに購入済みのデータが保存されているかもしれません。もしこのような状況であれば、スケーラブルな定量戦略向けデータ分析を実行するために、そのデータをOLAPシステムによる取り込みに適した形式へ変換する必要があります。幸い、このような必要性は定量取引分野では一般的な作業に過ぎず、DuckDBを使用すれば簡単に実行できます。
すでにシステムにDuckDBがインストールされていると仮定すると、DuckDBのCLIを使用して、DuckDBシェル内で以下の単純なコマンドを実行できます。
INSTALL sqlite; LOAD sqlite; ATTACH '<path/to/your/sqlite.db>' AS sqlite_db (TYPE SQLITE); USE sqlite_db; EXPORT DATABASE '<folder_name>' (FORMAT PARQUET);
<folder_name>は、変換されたデータをローカルファイルシステム内のどこに保存するかを指定する、任意のフォルダ名です。このコマンドを実行すると、指定したフォルダ内に複数のParquetファイルが生成されます。各データベーステーブルごとに1つのファイルが作成され、システムテーブルも含まれます。さらに、schema.sqlとload.sqlの2つの追加ファイルも生成されます。

図2:すべてのSQLiteテーブルがParquetファイルへ変換されたことを示すPowerShell出力
「schema.sqlファイルには、データベース内に存在するスキーマ定義文が含まれています。データベースを再構築するために必要となる、CREATE SCHEMA、CREATE TABLE、CREATE VIEW、CREATE SEQUENCEコマンドが含まれています。
load.sqlファイルには、CSVファイルから再びデータを読み込むために使用できるCOPY文のセットが含まれています。このファイルには、スキーマ内に存在する各テーブルについて1つのCOPY文が含まれています。」(DuckDBドキュメント)
また、特定のテーブルだけを変換したい場合もあるでしょう。実際、多くの場合はこちらの方が一般的になるでしょう。なぜなら、一部のテーブルはデータ分析ではなく、取引に関連しているためです。たとえば、「trade」テーブルと「strategy」テーブルは、それぞれ取引メタデータの保存、およびエキスパートアドバイザー(EA)へリアルタイムの戦略更新を提供するために使用されています。
コマンドラインから単一のテーブルを変換する方法は以下の通りです。データベース全体をエクスポートする代わりに、対象となるテーブルをoutput.parquetへCOPYします。
duckdb -c "INSTALL sqlite; LOAD sqlite;
ATTACH 'my_database.db' AS sqlite_db (TYPE SQLITE);
COPY sqlite_db.my_table TO 'output.parquet' (FORMAT PARQUET);" 出力フォルダ内のテーブルがParquetファイルへ変換されたら、通常のデータベーステーブルと同じように、従来のSQLを使用してクエリできます。たとえば、スコアリングシステムの結果を保存していたcoint_rankテーブルには、以下のようにアクセスできます。
SELECT timeframe,lookback,assets,rank_score FROM 'parquet.database/coint_rank.parquet' LIMIT 10;
コマンドライン出力はSQLiteの場合と似ています。

図3:Parquetデータベースからテーブルをクエリした後のDuckDB出力例
これ以降、これらのParquetファイルに対して、通常のデータベース操作を実行できます。たとえば、「glob」パターンを使用して複数のファイルを一度にクエリしたり、ビューを作成したり、JOINを実行したりできます。DuckDBのウェブサイトには、Parquetファイルの読み書きについて包括的なドキュメントがあります。
本連載では、ほとんどの場合、Pythonを使用してこれらのParquetファイルをクエリします。これはPython Relational APIによって支えられています。このAPIには、専門的な遅延評価型の分析関数が豊富に用意されています。これにより、Parquetファイルを非常に高速にスキャンできます。なぜなら、実行を開始するメソッドが呼び出されるまで、ファイルのメタデータだけを読み取るためです。
これはSQLite上でCSVファイルを読む場合とは異なるのか
前回の連載で設定した目標、つまり数学的または技術的な説明を避け、取引戦略そのものに焦点を置くという方針は維持します。しかし、ParquetファイルストレージとCSVファイルストレージの違いを理解することは重要です。
メタデータ
CSVはプレーンテキスト形式で、人間が読み取り可能な形式です。一方、Parquetはバイナリ形式であり、各ファイル内に大量のメタデータが埋め込まれています。DuckDBシェルで以下のクエリを実行することで、この埋め込まれたメタデータを簡単に確認できます。
.mode line select * from parquet_metadata('parquet.database/coint_rank.parquet');
自分自身のフォルダ名とParquetファイル名に置き換えてください(最初の「.mode line」は、出力が切り詰められることなく、1項目につき1行で、すべてのメタデータを表示できるようにするためだけのものです)。このクエリを実行すると、以下のような縦方向のリストが表示されるはずです。
file_name = parquet.database/coint_rank.parquet row_group_id = 0 row_group_num_rows = 7505 row_group_num_columns = 12 row_group_bytes = 533723 column_id = 0 file_offset = 0 num_values = 7505 path_in_schema = tstamp type = INT64 stats_min = 1760920476215768 stats_max = 1765924659832034 stats_null_count = 0 stats_distinct_count = NULL stats_min_value = 1760920476215768 stats_max_value = 1765924659832034 compression = SNAPPY encodings = PLAIN index_page_offset = NULL dictionary_page_offset = NULL data_page_offset = 4 total_compressed_size = 30166 total_uncompressed_size = 60069 key_value_metadata = {} bloom_filter_offset = NULL bloom_filter_length = NULL min_is_exact = true max_is_exact = true row_group_compressed_bytes = 1 geo_bbox = NULL geo_types = NULL
Parquetに変換したcoint_rank SQLiteテーブルのスキーマは、以下のとおりであることを覚えておいてください。

図4:statarb-0.5.dbダイアグラム内のcoint_rankテーブルのスクリーンキャプチャ
DuckDBでParquetファイルをクエリする場合と、CSVファイルをクエリする場合の違いをよりよく理解するために、p_value列に対して何らかの集計処理やソート処理を実行したいと仮定しましょう。CSVの場合、システムはp_valueフィールドに到達するためだけに、各行について列tstamp、timeframe、lookback、assetsを読み取る必要があります。一方、Parquetファイルでは、その列指向ストレージモデルにより、システムはまず列のメタデータを読み取ることができます。システムは、tstampに対応するcolumn_id = 0を読み取ります。各列は個別に保存されており、今回要求しているのはp_valueだけなので、システムは必要なp_value列のcolumn_idを見つけるまで、次の列へ直接進むことができます。そのため、クエリへの回答に必要のない大量のデータを読み取ることを回避できます。
クエリの内容によっては、埋め込まれたメタデータ内に統計情報が存在するため、ファイルを一切読み取る必要がない場合もあります。なぜなら、システムはメタデータだけを読み取ることで、どのファイルを読み取る価値があるかを判断できるからです。たとえば、2024年のデータを要求するクエリを実行したとしましょう。上記のtstamp列のメタデータには、stats_minおよびstats_maxというセクションが含まれており、そこには以下の値が格納されています。
stats_min = 1760920476215768 (Monday, October 20, 2025, at 12:34:36.215 AM)
stats_max = 1765924659832034 (Tuesday, December 16, 2025, at 10:37:39.832 PM)
tstamp列の最小値は、かなり未来の日付から始まっています。メタデータによってデータが存在しないことがシステムに通知されているため、システムはこのファイルを開く必要すらありません。CSVファイルは、ファイル全体を読み込むまで、その中身が何であるかを認識しません。
圧縮
さらに、最適化されたストレージシステムが無料で利用できます。
圧縮されていない合計サイズ= 60,069
圧縮後の合計サイズ= 30,166
Parquetはファイルサイズを半分に削減することに成功しました。
重要な違いは、システムが情報をどのように探すかという点にあります。CSVは、特定の単語を探すために本のすべてのページを読まなければならない本のようなものです。Parquetは、完璧な索引と各章ごとの概要を持つ本のようなものです。つまり、圧縮された本と言えるでしょう。
ゼロコピーParquetリーダー
不要なファイル全体の読み取りを避けるために最初にメタデータを読み取り、さらに圧縮によって性能を向上させることに加えて、もう1つ、性能を劇的に向上させる機能があります。それがゼロコピーリーダーです。DuckDBとParquetファイル形式について調べると、この表現を頻繁に目にするでしょう。これを実装するには高度な低レベルプログラミング技術が必要ですが、エンドユーザーの視点からは簡単に理解できます。
CSVファイルを読み取る場合、SQLiteのようなシステム(およびその他のRDBMS)は、ディスクからデータをバッファへ読み込み、CSVテキストを解析し、そして独自の内部メモリ形式でデータの新しいコピーを作成する必要があります。クエリは、その2番目のコピーに対してのみ実行できます。これはインポート処理のようなものです。読み取り、変換、コピー、そして最後にクエリを実行します。
DuckDBは、Parquetページをメモリマッピングすることによって、最小限のコピーでデータを処理できます。つまり、ファイルを直接メモリへマッピングし、2次的なコピーを作成しません。
分析用の市場データを保存する
ご存じのように、MetaTrader 5には非常に効率的なローカルストレージ・キャッシュ機構があり、以前に要求されたデータについてはサーバーへアクセスしません。しかし、私たちは市場データを常にParquetファイルとして利用可能な状態にしておきたいと考えています。この方法により、過去データのポータビリティが確保されます(別のマシンで作業したい場合や、データを外部ドライブに保存したい場合があります)。さらに、市場データの有料サービスに加入している場合、その内容はおそらくMetaTrader 5のヒストリカルデータと結合する必要があります。これらの理由から、今後は以下のワークフローを採用します。MetaTrader 5からデータを取得し、Parquetファイルとしてローカルに保存します。
非正規化
テーブルまたはデータベース全体をParquetへ変換することは、性能、使いやすさ、そしてクエリの単純化に大きく役立ちます。しかし、私たちのスキーマは拡張性がありません。現在、価格履歴はmarket_dataとsymbolの2つのテーブルに分かれています。SQLiteでは、データ整合性を確保し、ストレージ容量を削減するために正規化されていました。しかし、OLAPスキーマでは速度を優先するため、テーブル間のコストの高いJOINを減らす必要があります。この要件のため、通常は非正規化されます。価格履歴は私たちの主要なデータ分析コンポーネントであるため、まずここから非正規化を開始します。
パーティション
しかし、大規模なデータセットに適したスケーラブルなストレージ・データベースシステムを構築するには、非正規化だけでは十分ではありません。間もなく、複数のソースからより多くの価格履歴データを取得し始めます。数週間後には、たとえば50個の銘柄、10以上の時間足についてティックデータを保有している可能性があります。これはかなりの量のデータです。しかし、定量戦略の場合、500または5000銘柄、20以上の時間足についてティックデータを分析する必要があったとしても、これは異常な規模ではありません。したがって、拡張可能なストレージスキーマが必要です。パーティションは、非正規化した履歴データを保存するために使用する最初の技術です。
このデータに対して書き込みをおこなう予定はありません。これは読み取り専用ストレージです。そのため、DuckDBの単一ファイルデータベースではなく、Parquetファイルにデータを保存します。複数のソースからデータを取得し、それぞれを分離して整理された状態で保持し、要求された時点で自由に結合できます。このような用途には、DuckDBの単一ファイルよりもParquetファイルのほうが適しています(DuckDBの単一ファイルデータベースは、書き込みが必要になる別のシナリオで、後ほど役立つでしょう)。
しかし、これらのファイルをどのように命名するのでしょうか。これは、単純な作業に見えて実は重要な問題です。なぜなら、ファイル名は一貫性があり、統一され、クエリで使いやすいものである必要があるからです。ファイル名には銘柄名(ティッカー)、時間足、履歴範囲を含めるべきでしょうか。データソースを参照情報として含めるべきでしょうか。他に考慮すべき重要な情報を見落としているでしょうか。
幸い、この問題はすでに解決されています。もちろん、絶対的なルールが存在するわけではありません。しかし、実際の必要性から導かれた多くの経験則やベストプラクティスがあります。私たちは、この蓄積された経験を利用します。
ファイルシステムフォルダを仮想列として使用する
私たちはファイルシステムのフォルダ(ディレクトリ)を仮想列として使用します。このような構成における最も重要な経験則は、フォルダ構造が、最も頻繁に予想されるクエリの構造を可能な限り反映すべきであるということです。したがって、この構造は必要性によって大きく変化する可能性があります。ここで示す構造を出発点として使用し、システムに慣れ、新しい要求が発生するにつれて、自分自身の構造を開発していくことができます。このようなデータ整理方法はHiveパーティションと呼ばれます。これは非常に高性能で、直感的、柔軟性があり、DuckDBドキュメントでも詳しく説明されています。
「Hiveパーティションとは、パーティションキーに基づいてテーブルを複数のファイルへパーティションするために使用されるパーティション戦略です。ファイルはフォルダ内に整理されます。各フォルダ内では、パーティションキーの値はフォルダ名によって決定されます。」
Hiveパーティションは、大規模データセットを整理するためによく知られた方法です(そして、私たちはデータセットがすぐに大規模になることを想定しています)。DuckDBや、Apache Spark、AWS Athenaを含むその他のシステムは、これを仮想列として理解できます。ここでは互換性を重視しています。
現在、6つのパーティションキーがあります。
- source =データプロバイダーまたはブローカーサーバー名
- klass =アセットクラス(classはPythonの予約済み識別子であるためklassという名前にしています)
- ticker =銘柄名
- tf = 時間足
- year
- month
これにより、以下のような構造になります。
market_data\source=MetaQuotes-Demo\klass=Forex\ticker=AUDCAD\tf=H1\year=2026\month=2\data_0.parquet

図5:サンプルのHiveパーティション化された市場データフォルダのツリー構造
DuckDBにmarket_data/**/*.parquetを読み取るよう要求すると、フォルダパスに基づいて、すべてのキーに対応する列を自動的に結果セットへ追加します。また、
SELECT * FROM market_data WHERE ticker='AUDCAD' AND year=2025 のようなクエリを実行すると、システムは該当するフォルダ内のファイルだけを開きます。2024年のデータについては、そもそも考慮すらしません。これにより、クエリは大幅に高速になります。インターネット上の一部の情報やAIアシスタントでは、履歴データが増えるにつれて10倍から100倍高速になると言われています。
このような構成にすると、立ち上げたばかりのデータストアの保守も容易になることに、すでに気付いているかもしれません。たとえば、あるデータのまとまり(たとえば年単位または月単位)を削除または更新したい場合、単純にフォルダを削除するだけで実行できます。
Parquetファイル内の列名は、小文字で、スペースを含まないようにしてください(open、high、low、close、tick_volumeなど)。DuckDBは、多くの設定においてデフォルトで大文字と小文字を区別します。また、タイムスタンプは常にUTCで保存してください。MetaTrader 5のブローカー時刻は異なる場合があり、オフセットを認識するタイムスタンプとオフセットを認識しないタイムスタンプを混在させると、夏時間の変更によってタイムスタンプが突然ずれる可能性があります。タイムゾーンについての最終的な推奨事項は、この記事の最後を参照してください。
この記事のフッターには、MetaTrader 5ターミナルから価格データを取得し、上記で説明したキーに基づいて仮想列を追加し、圧縮されたParquetファイルとしてネストされたフォルダ階層へエクスポートするPythonスクリプトがあります。このスクリプトは、銘柄単位、または銘柄「path」(MetaTrader 5における名前付きクラス内の銘柄グループ)単位でダウンロードします。また、分析に必要な不足しているデータだけを取得し、欠落部分を補完します。さらに、ブローカーサーバーへのリクエストが適切な間隔になるよう、リクエスト間に待機時間を設けます。
データが用意できれば、SQLを使用して通常のリレーショナルデータベースを問い合わせるのと同じくらい簡単にクエリを実行できます。たとえば、ローカルで利用可能なすべてのEURUSDの時間足から陽線を探す場合、以下のようになります。
SELECT * FROM read_parquet('data/**/*.parquet', hive_partitioning = 1) WHERE ticker = 'EURUSD' AND year = 2026 AND close > open; -- Find bullish candles
しかし、この単純さと容易なクエリ実行性にもかかわらず、業界レベルのOLAPシステムが持つすべての利点を得ることができます。また、これまで使用してきたPearson相関係数などの専門的な統計ネイティブ関数も利用できます。詳細についてはDuckDBのドキュメントを参照してください。
可能性のある戦略を探すためのデータ探索
ここでの目的は、トレード戦略の有効性を評価することではなく、OLAPシステム上でどのような種類のクエリを実行したいのかを明確にすることです。
トレード機会を探すためにデータを分析する場合、最も良い方法は、仮説から始めることだと言えます。 調査する価値のあるものを発見した場合、それをバックテストし、最終的には有望な結果が得られるかを確認します。たとえば、連載「共和分株式による統計的裁定取引」では、「半導体業界の流動性の高い株式とNvidia株の間に、持続可能な共和分関係を見つけられる可能性がある」という仮説を立てました。同様に、今回も仮説を持って探索的データ分析を開始します。
物事を単純化し、結果に焦点が移ってしまうことを避け、代わりにプロセスそのものに焦点を置くため、今回は非常によく知られた仮説「ゴールデンクロスのモメンタム戦略は、FXとインデックスでは大きく異なるリターンを生み出す可能性がある」から始めます。この仮説を検証するため、ブローカーサーバー(この場合はMetaQuotes-Demoサーバー)で利用可能なすべてのFXおよびインデックスの銘柄に対してこの戦略を適用します。ルックバック期間は1250日とし、30日先のフォワードリターンを確認して、それらがどのように比較されるかを調べます。
ゴールデンクロスのモメンタム戦略とは
これまでにトレード戦略を調査したことがある方は、おそらくゴールデンクロス戦略が何であるかはすでにご存じでしょう。とはいえ、この戦略で使用される2つのインジケータのパラメータにはいくつかのバリエーションがあるため、この例で使用する値を明確にしておきます。
ゴールデンクロスとは、長期的な時間軸に適用された単純移動平均のクロスオーバーに対する、単なる洒落た呼び方に過ぎません。これは、短期移動平均が長期移動平均を上抜けることで発生するテクニカル分析上のパターンを識別します。その意味は非常に直感的で、弱気モメンタムから強気モメンタムへの反転の可能性を示します。ゴールデンクロスは、価格が底を形成した可能性があり、その時点以降、新しい長期的な強気相場の開始が期待できることを示している、と言うこともできます。通常、短期および長期の単純移動平均(SMA)として、それぞれ50日SMAと200日SMAが使用されます。
以下の例では、赤い線が短期、つまり高速SMAを表し、青い線が長期、つまり低速SMAを表しています。

図6:2012年12月3日に成功したゴールデンクロスを示した日足時間足のUSDJPY
ゴールデンクロスは、他のあらゆるインジケーターと同様に、誤ったシグナルを生成する可能性があることに注意してください。これは市場が横ばいに推移している場合によく発生します。

図7:2023年1月26日に失敗したゴールデンクロスを示した日足時間足のAUDUSD
ゴールデンクロスは株式市場に起源があります。これは、小売トレーダーの間で人気のある多くのトレード戦略やインジケーターにも共通する特徴です。しかし、現在では暗号資産やFXにも使用されています。この場合、20日/50日のような、より短いSMAを使用して適用されることが一般的です。また、トレーダーが考案し、試行できる限り多くのこれらの値のバリエーションが存在します。
ゴールデンクロス検証スクリプト
いくつかの銘柄でゴールデンクロスを検証することは比較的簡単ですが、すべてのForexおよびインデックスの銘柄に対して同時に検証することこそ、スケーラブルなデータストアとOLAPシステムを必要とする正確な理由です。つまり、一般的なノートPC上でも、数秒で実行したいのです。この記事のフッターには、この作業を支援するシンプルなPythonスクリプトがあります。このスクリプトは、自分自身の実験や開発のための出発点として利用してください。
これは、そのメイン関数です。選択したルックバック期間を使用して、銘柄リストに対してゴールデンクロス分析を実行するメソッドです。また、ポートフォリオ名を指定することもできます(任意のプロットで使用されます)。さらに、結果をCSVファイルへエクスポートするかどうかも選択できます。このCSVファイルは、将来的にバックテスト対象をスクリーニングする際に、EAによって使用されることを想定しています。
def golden_cross(self, symbols, lookback=2500, portfolio_name="Portfolio", plot=True, export_csv=True): # 1. Ensure we have data self._ensure_data(symbols, "D1", lookback) # 2. Query the Hive-partitioned Parquet files parquet_path = f"{self.base_path}/**/*.parquet"
最初に、Parquetデータストアに十分な価格データがすでに保存されていることを確認します。不足している場合は、MT5からデータを取得してデータストア内の欠落部分を補完します。

図8:不足データを要求する戦略アナライザーPythonスクリプトの出力画面
これはスクリプトの中核部分です。上で作成したファイルシステムパーティション上でゴールデンクロスを計算し、30日先のフォワードリターンを検証するSQLクエリです。
query = f"""
WITH base_ma AS (
SELECT
time, ticker as symbol, close,
AVG(close) OVER (PARTITION BY ticker ORDER BY time ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) as ma50,
AVG(close) OVER (PARTITION BY ticker ORDER BY time ROWS BETWEEN 199 PRECEDING AND CURRENT ROW) as ma200
FROM read_parquet('{parquet_path}', hive_partitioning=1)
WHERE ticker IN ({str(symbols)[1:-1]})
),
lagged_data AS (
SELECT
*,
LAG(ma50) OVER (PARTITION BY symbol ORDER BY time) as prev_ma50,
LAG(ma200) OVER (PARTITION BY symbol ORDER BY time) as prev_ma200,
LEAD(close, 30) OVER (PARTITION BY symbol ORDER BY time) as future_price
FROM base_ma
),
signals AS (
SELECT
*,
((future_price - close) / close) * 100 as pct_return_30d
FROM lagged_data
WHERE prev_ma50 < prev_ma200 AND ma50 > ma200
)
SELECT * FROM signals;
""" クエリの下部にあるこの行に注目してください。
WHERE prev_ma50 < prev_ma200 AND ma50 > ma200
簡単に分かるように、これはゴールデンクロス、つまりSMA(単純移動平均)のクロスオーバーの定義を含んでいます。私たちが構築しているシンプルなシステムは、一般的なノートPC上で動作し、数百万行もの過去データをスキャンして、この現象が発生したすべての時点を検出し、その時点から次の30日間の勝率を計算できます。これにより、主観的なチャートパターンの視覚的な判断を、客観的な統計的測定へと変換しています。そして、選択した銘柄、選択したルックバック期間、時間足に対して、その戦略が利益を生み出すかどうかを数秒で確認または否定できます。株式、インデックス、通貨などのグループ全体を、価値があると思う任意の組み合わせでバックテストできます。そして、それを従来のPythonループよりもはるかに高速に実行できます。有望なものを発見した後に初めて、MT5環境でバックテストを実行するためのEA開発、最適化、そして最終的な検証のためのデモ口座での実行に労力を投入することになります。このような事前分析を活用し、習熟することで、定量的トレードへの取り組みを真剣に進めている場合、数日、あるいは数週間に及ぶ大変な作業を節約できる可能性があります。
最後に、スクリプトは結果を銘柄ごとに集計します。
results = self.con.execute(query).df() if results.empty: print("No Golden Cross signals found in the dataset.") return # 3. CSV Export Logic (Aggregated by Symbol) if export_csv: summary = results.groupby('symbol').agg( frequency=('symbol', 'count'), avg_return_30d=('pct_return_30d', 'mean') ).sort_values(by='avg_return_30d', ascending=False) filename = f"golden_cross_summary.csv" summary.to_csv(filename) print(f"\nSummary exported to {filename}") print(summary) # Display table in console for quick check
執筆時点でMetaTrader 5で利用可能だったインデックス銘柄の出力です。

図9:銘柄ごとのサマリーを表示した戦略アナライザーPythonスクリプトの出力画面
同じ上記データはCSVファイルにも保存されます。
FXとインデックスにおけるゴールデンクロス
前述したように、ゴールデンクロスのシグナルをForexとインデックスに適用した場合に、結果がどの程度異なるのかを確認するこの簡単な検証の目的は、現在構築しているOLAP対応のデータストアが、既知のトレード戦略の有効性検証だけでなく、新しいトレード機会の探索にもどのように役立つかを示すことです。
日足において、過去1250取引日、つまり約5年間について、FXとインデックスにおける30日先のフォワードリターンは以下のようになりました。

図10:FXにおける日足(D1)、ルックバック期間1250日のゴールデンクロス30日フォワードリターンのプロット

図11:インデックスにおける日足(D1)、ルックバック期間1250日のゴールデンクロス30日フォワードリターンのプロット
FXでは、実用的な観点では結果はほぼブレークイーブンでした(まだ取引コストは含めていません)。一方、インデックスでは、図9で示した上位8つのインデックス銘柄を中心に、いくつかの利益を確認できます。とはいえ、日足ではシグナル数は非常に少ないです。
データは私たちの仮説について何を示しているのでしょうか。
人気のある戦略ではありますが、ゴールデンクロスはインデックスに対してより良く機能するように見えます。この戦略が株式市場の文脈で開発されたことを思い出してください。FXではパフォーマンスは低調です。少なくとも、日足時間足および1250日のルックバック期間という条件ではそう言えます。
この最後の一文こそが、本稿の主題を理解するうえで重要なポイントです。つまり、私たちはデータ分析を非常に高速かつ容易に実行できる必要があります。なぜなら、ニッチな機会を発見するために、多くの異なるパラメータに対して分析を実行し、それらを予想外の方法で再組み合わせる必要があるからです。さらに、ライブトレードを監視しながら、これらの分析を実行する必要に直面する可能性さえあります。
これこそが、ここで構築しているOLAP対応データストアの本質的な目的です。
タイムゾーンに関する最後の推奨事項
私たちは時系列データを扱っていることを忘れないでください。日時の一貫性は極めて重要です。私たちは常にタイムスタンプを基準としてテーブルを結合しています。もし価格情報の時刻がずれていれば、分析結果は損なわれます。そして、さらに悪いことに、コード内に防御策が存在しない場合、エラーを発生させることなく静かに失敗する可能性があります。最も一般的な間違いの1つは、データをローカル日時で保存すること、またはUTC時間のデータと混在させることです。さらに、データプロバイダーのサーバー側で発生する可能性のある夏時間の存在も、別の問題の原因になります。これらの問題を避ける最善の方法は、データストア内のすべてをUTCで維持し、Pythonでのクエリでもこの一貫性を守り、常にDuckDBシステムをUTC対応モードで初期化することです。
DuckDBでは、一貫性を確保する方法は、データベースをタイムゾーン非依存として扱うことです。つまり、すべてをUTCで保存し、タイムゾーン変換は分析の最後の段階、または必要であれば可視化の段階でのみ処理します。DuckDBセッション全体をUTCに強制するには、DuckDB接続を初期化した直後に以下の行を追加します。
con = duckdb.connect()
con.execute("SET TimeZone='UTC';") これにより、SQL内部のnow()やtoday()のような関数は、コンピュータのローカル時間ではなくUTCを返すようになります。
もし将来的に、別のデータソース(たとえば別のブローカー)とデータを比較する必要がある場合は、AT TIME ZONE構文を使用してください。これにより、比較対象の両方がUTC上で揃えられることが保証されます。
SELECT
time AT TIME ZONE 'UTC' as utc_time,
close
FROM read_parquet('...') 最後に、Pythonのdatetimeオブジェクトをクエリへ渡す場合には注意してください。
以下のようにはしないでください。
con.execute("SELECT * FROM df WHERE time > ?", [datetime.now()]) DuckDBへ送信する前に、Python側のオブジェクトがタイムゾーン情報を持っていることを必ず確認してください。
con.execute("SELECT * FROM df WHERE time > ?", [datetime.now(timezone.utc)])
以下に添付されているサンプルスクリプトでは、すでに__init__メソッド内でタイムゾーン設定を追加しています。
class StrategyAnalyser: def __init__(self, base_path="market_data"): self.base_path = base_path self.con = duckdb.connect(database=':memory:') # FORCE UTC AT THE START self.con.execute("SET TimeZone='UTC';") self.downloader = DataDownloader(base_path)
結論
ここまでで、Parquetファイルの集合を基盤とし、クエリしやすく保守しやすいHiveパーティショニングされたファイルシステム上に構成する、OLAP対応データストアについて、最小限でありながらスケーラブルかつ将来性のある構成を提案しました。また、SQLite、Postgres、MySQLなどの他のRDBMSに保存されていた既存データを、この新しいシステムへ変換する簡単な方法も示しました。
本連載記事で今後開発していく内容に向けて、複数のデータソースを容易に統合し、データストアの性能を向上させるため、ローカルに保存された過去価格データセットを構築することを提案しました。
最後に、現在のブローカーサーバー(MetaQuotes-Demo)で利用可能なすべての銘柄を対象に、FXとインデックスという2つの異なるアセットクラスへ適用した場合の、よく知られたゴールデンクロスモメンタム戦略を比較する分析例を提示しました。これにより、この分析の目的は、OLAP対応データストアと、それを支えるシステム(DuckDB)が、一般的なノートPC、Parquetファイルの集合、そして数行のSQLコードだけで実行できる分析の種類を示すことにあることが明確になります。
| ファイル名 | 説明 |
|---|---|
| data_downloader.py | 必要なデータでローカルデータストアを最新状態に維持するためのPythonスクリプト |
| strategy_analyser.py | 銘柄リストに対してゴールデンクロスのサンプル分析を実行するためのPythonスクリプト |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21679
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初心者からエキスパートへ:MQL5におけるマルチタイムフレーム構造確認による流動性戦略の強化
バックテスト過剰適合を防ぐ統合検証パイプライン ― V-in-V・CPCV・CSCVによる堅牢な戦略検証
MetaTrader 5機械学習の設計図(第8回):パージ済み交差検証と試行枝刈りを用いたベイズ最適化によるハイパーパラメータ最適化
MQL5経済指標カレンダーを用いたニュースフィルタリング(第2回):ニュースリリース中に管理ポジションを停止する
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索