English Русский Deutsch
preview
Python + MetaTrader 5:データ、機能、プロトタイプのための高速研究フレームワーク

Python + MetaTrader 5:データ、機能、プロトタイプのための高速研究フレームワーク

MetaTrader 5統合 |
37 2
MetaQuotes
MetaQuotes

はじめに

Pythonはデータを扱うための最も便利なツールの一つになっています。Pythonは幅広いライブラリを提供しており、統計分析、仮説検証、結果の可視化を短時間かつ低コストで実行できます。金融市場の分析では特に重要になります。金融領域では、データ処理の速度だけでなく、分析から実用的な結論へ素早く移行できる能力も重視されます。

MetaTrader 5Pythonとの直接統合機能を備えており、これにより市場データを扱う実務的な作業の可能性が大きく広がります。研究者や開発者は、慣れ親しんだPythonツール群を用いて価格データを分析し、統計モデルを構築し、実験的な仮説を検証しながらも、取引プラットフォームとの接続を維持できます。このアプローチはワークフローをより柔軟にし、「データ→仮説→モデル→実運用」という一貫したサイクルを支えます。

MetaTrader 5 + Python

本記事では以下を扱います。

  • PythonMetaTrader 5の統合方法
  • Pythonを用いた金融データ分析および仮説検証の方法
  • 小規模なモデルを構築・学習し、その結果をONNX経由でEAへ移行する方法

これにより、研究実験から実際のトレードシステムへの実装までを一連の流れとして扱えるようになります。

1. インストールと接続

データ分析に進む前に、PythonMetaTrader 5を併用するための作業環境を順に準備する必要があります。作業自体は単純ですが、設定ミスを避けることが重要です。初期設定を正しくおこなうことで、後々発生する多くの細かな問題を回避できます。

まず最初に、公式サイトからMetaTrader 5ターミナルの配布パッケージをダウンロードしてインストールします。

次に、最新のPythonバージョンを用意します。本記事執筆時点では3.14.3です。インストール時には、PythonPATH環境変数に追加するオプションを必ず有効にしてください。これにより、追加設定なしでコマンドラインからインタプリタを利用できるようになります。

Pythonのインストール

重要なポイントは環境の分離です。データやモデルを扱うプロジェクトは、依存関係がすぐに複雑化します。そのため、再現性と保守性を確保するために、プロジェクトごとに独立した仮想環境を作成することが推奨されます。Pythonでは標準機能であるvenvを使用して対応できます。

ワークフローは以下の通りです。

  1. まずコマンド実行環境を開きます。最も簡単な方法はWin+Rを押し、表示されたウィンドウにcmdと入力してEnterを押すことです。これによりWindowsコマンドプロンプトが起動します。PowerShellを使用しても問題はありません。同じ手順で動作します。
  2. プロジェクトディレクトリへ移動します。これは次のコマンドでおこないます。
  3. cd /path/to/your/project
  4. 仮想環境を作成します。
  5. python -m venv integration

    そして有効化します。

    integration\Scripts\activate

    これ以降、インストールされるパッケージはすべて現在のプロジェクト内に隔離されます。

  6. MetaTrader 5と連携するためのPythonモジュールをインストールします。
  7. pip install MetaTrader5
  8. 金融データ分析を本格的に行うために、基本的かつ実用的なライブラリを一式導入するのが合理的です。これにより、データ処理、モデル構築、テクニカル分析の主要タスクをカバーできます。

まずNumPyをインストールします。これは数値計算の基盤となるライブラリであり、その後のすべての処理の土台になります。

次にPandasを導入します。これはテーブルデータおよび時系列データを扱うための主要ツールであり、価格分析には不可欠です。

可視化にはMatplotlibSeabornを使用します。Matplotlibはグラフを細かく制御でき、Seabornは統計的に意味のある可視化を簡単に作成できます。両者を併用することで、単なる数値ではなく「市場の構造」を視覚的に把握できます。

機械学習にはScikit-Learnを追加します。これはモデル構築と検証のための定番ツールであり、初期プロトタイプや基本的な戦略構築に適しています。

実務的な市場分析のためにTAライブラリも導入します。これはテクニカル指標を迅速に追加するための便利な手段であり、古典的な指標を自前実装する手間を省きます。

ライブラリのインストールは一括で実行できます。

pip install numpy pandas matplotlib seaborn scikit-learn ta

さらにpytzライブラリも追加します。一見すると補助的ですが、実務ではタイムゾーン処理において非常に重要です。

pip install pytz

金融データは厳密に時間依存です。取引所は異なるタイムゾーンで稼働しています。

デフォルトでは、Pythondatetimeオブジェクト生成時にシステムのローカル時間を使用します。この挙動は日常用途では便利ですが、金融領域では体系的な誤差の原因となります。MetaTrader 5はティックおよびバーの時間をUTC形式で保持しており、ローカルタイムゾーンの概念を持ちません。

ここで典型的な不整合が発生します。モデルはある時間基準で動作し、データは別の基準で提供されます。よって、実際の分析結果にズレが生じます。

そのためルールは明確です。時間に関するすべての処理はUTCで統一する必要があります。datetimeオブジェクトは明示的にUTCで生成し、ローカル値は必ず統一された基準に変換します。これにより、データとモデルが同一の時間基準に整合されます。

このような場合にはpytzを使用するのがベストプラクティスです。暗黙的な変換を排除し、システムの挙動を予測可能にします。

MetaTrader 5から取得されるデータはすでにUTCです。このデータを補正する必要はありません。正しく解釈し、モデルのロジックと整合させることが重要です。金融分野では時間は単なるラベルではなく、座標軸です。ここでの誤りは分析結果を歪めます。

この構成は一見するとシンプルですが、実務上の典型的なタスクの約80%をカバーします。これは冗長性を減らし、効率を高めるという基本方針に基づいています。

現在の環境を終了する必要がある場合は、標準コマンドを使用します。

deactivate

この時点でインフラは完全に準備完了です。ターミナル、インタプリタ、必要なライブラリが整備され、環境は分離され再現可能になっています。これは分析、仮説検証、そしてトレードモデル開発へと段階的に進むための堅牢な基盤となります。


2. データの読み込み

インフラを整えた後は、最初の実践的なステップであるプログラムの作成に移ります。ここには厳密な制約はなく、慣れたエディタであればどれでも使用できます。ただし統合環境という観点では、Pythonにも対応しており、作業全体を一つの枠組み内で完結できるMetaTrader 5の内蔵エディタ「MetaEditor」を使用するのが合理的です。

MetaEditorやターミナルからPythonスクリプトを直接実行するためには、プラットフォームの設定で一度だけPythonインタープリタのパスを指定すれば十分です。これは基本的な操作ですが、ここには重要な点があります。すでに作成された仮想環境を使用している場合、そのパスはグローバルなPythonインストールではなく、その仮想環境のインタープリタを指している必要があります。

この方法によりプロジェクトの分離が維持され、すべての依存関係が管理された状態に保たれます。そうしなければ、同じコードでも実行環境によって挙動が微妙に異なるといった問題が発生する可能性があります。金融の世界では、これは許容できないリスクです。

最初の段階では、PythonMetaTrader 5 → 過去データという基本的な接続を構築します。このタスクは表面的には単純ですが、本質的には非常に重要です。スクリプトからターミナルに接続し、指定した銘柄と時間足の価格データを取得します。この瞬間から、意味のある分析が始まります。

スクリプトの構造はブロックごとに分けて構築するのが合理的です。まず必要なライブラリを接続します。これにより作業用のツールが形成されます。

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import pytz
import seaborn as sns
import matplotlib.pyplot as plt
import ta

次に、MetaTrader5モジュールを通じてターミナルへの接続を初期化します。これはシステムへのエントリーポイントです。接続が確立されない場合は、続行する意味はありません。そのため、ステータスチェックは直ちに、そして妥協なく実行されます。

# Display data on the MetaTrader 5 package
print("MetaTrader5 package author: ", mt5.__author__)
print("MetaTrader5 package version: ", mt5.__version__)

# Connection to MetaTrader 5 terminal
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

次のステップは、時間間隔を設定することです。ここではUTCゾーンを明示的に指定するためにpytzを使用します。これは単なる実装上の細部ではなく、必須条件です。ターミナルは価格データをUTCで保存しており、Python側でのわずかなズレでもデータのバイアスにつながります。このエラーは表面上は静かに発生しますが、その影響はシステム全体に及びます。

# Set time zone to UTC
timezone = pytz.timezone("Etc/UTC")
# Create 'datetime' objects in UTC time zone to avoid the implementation of a local time zone offset
utc_from = datetime(2020, 1, 1, tzinfo=timezone)
utc_to = datetime.now(timezone)  # Set to the current date and time

その後、過去データをリクエストします。例ではEURUSDの1時間足(H1)を使用しています。ここで注目すべき点は銘柄名です。これはターミナル内での表記と完全に一致している必要があり、接尾辞や接頭辞を含めて厳密に一致していなければなりません。

# Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

出力は、価格とタイムスタンプを含む構造体の配列であり、フィルタや解釈が加えられていない生の市場データです。初期分析では、この生データがそのまま必要になります。

その後、ターミナルとの接続は正しく終了されます。適切に接続を終了することで、正常終了は次回以降の起動時における隠れた障害を防ぎ、システムの挙動を予測可能にします。

# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()

その後、結果の基本的な確認をおこないます。データが取得できている場合は、簡易的な検証のために最初のレコードが表示されます。取得できていない場合は、スクリプトは終了します。

# Check if data was retrieved
if rates is None or len(rates) == 0:
    print("No data retrieved. Please check the symbol or date range.")
    quit()
# Print the first 10 raw records for a quick data sanity check
print("Display obtained data 'as is'")
for rate in rates[:10]:
    print(rate)

さらに、可視化もおこないます。終値と出来高のチャートを構築します。この場合、出来高は別の軸に配置することが推奨されます。出来高軸の最大値は、観測された最大値の5倍のマージンを持たせて設定します。この手法は一見シンプルですが、非常に安定して機能します。ヒストグラムはチャートの下部に圧縮表示され、価格との視覚的な競合が解消されます。

その結果、終値のラインはクリーンで読みやすい状態を維持し、出来高は視覚的な負荷を増やすことなく情報として機能します。これはデータの完全性と可読性の間における典型的なバランスです。チャートは単に情報を含むだけでなく、余計なストレスなしに迅速に解釈できるものでなければなりません。

# Create a DataFrame from the retrieved tick data
rates_frame = pd.DataFrame(rates)
# Convert the timestamp column from seconds since epoch to datetime
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

# Use datetime as the DataFrame index for time series plotting and analysis
rates_frame.set_index('time', inplace=True)

# Plot closing price and tick volume
fig, ax1 = plt.subplots(figsize=(12, 6))

# Close price on primary y-axis
ax1.set_xlabel('Date')
ax1.set_ylabel('Close Price', color='tab:blue')
ax1.plot(rates_frame.index, rates_frame['close'], color='tab:blue', label='Close Price')
ax1.tick_params(axis='y', labelcolor='tab:blue')

# Tick volume on secondary y-axis
ax2 = ax1.twinx()  
ax2.set_ylabel('Tick Volume', color='tab:green')
max_tick = rates_frame['tick_volume'].max()
ax2.set_ylim(0, max_tick * 5)
ax2.plot(rates_frame.index, rates_frame['tick_volume'], color='tab:green', label='Tick Volume')
ax2.tick_params(axis='y', labelcolor='tab:green')

# Show the plot
plt.title('Close Price and Tick Volume Over Time')
fig.tight_layout()
plt.show()
fig.savefig('close_price.png')

終値変動

実務的な観点から見ると、この手順は一見すると単純に見えます。しかし実際には、これはデータ品質を確認する重要な工程です。モデルに何が入力されているのか(フォーマット、タイムスタンプ、値)を明確に把握できます。このような初期監査は、エンジニアリング的アプローチの典型例です。時間、精神的負担、そして特にトレードシステムにおいて重要なコストである資金を節約します。


3.仮説の検証と特徴量の選定

ここまでで過去の価格データが利用可能になったので、初期分析に進みます。まずは極めて単純で、ほとんど教科書的な仮説から始めます。「市場は直前のバーの動きを継続する傾向がある」というものです。短期的なダイナミクスにモメンタムが存在するかどうかを確認します。

Pythonツールキットを用いれば、このような仮説はわずか数行でテストできます。ロジックは次の通りです。終値の系列を取得し、バーごとの差分へと変換します。これにより、価格変動のダイナミクスが得られ、分析において本質的に重要な情報となります。

# Correlation analysis between adjacent bar moves
close = rates_frame['close'].to_numpy(dtype=float)
# last and next price move differences
diff = close[1:] - close[:-1]

次に、直前の変化との変化を表す2つの時系列を作成します。技術的には、これは配列を1要素分シフトすることで実装されます。その結果、各観測値は「市場が上昇(または下降)していた場合、その次のバーでは何が起きるのか」という単純な問いに対応するペアになります。

diff = np.column_stack((diff[:-1], diff[1:]))
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])

その後、これら2つの時系列の間でピアソンの相関係数が計算されます。正の相関は強いモメンタムを示し、負の相関は主に反転(プルバック)が優勢であることを意味します。

明確化のために、結果はSeabornを用いて可視化されます。相関ヒートマップを構築することで、数値的な詳細に踏み込むことなく依存関係の構造を素早く把握することができます。

correlation_matrix = data_matrix.corr('pearson')
plt.subplots(figsize=(3, 2))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Bar to Bar') 
plt.savefig('bar_to_bar.png')
plt.show()

ここでの重要なポイントは戦略そのものではありません。本質的にこれは単純すぎるものです。価値があるのは別の部分にあります。私たちは基本的な仮説検証のサイクルを示しています。仮説を立て、データを変換し、計算を実行し、結果を可視化する。このアプローチは分析に規律を与え、見た目だけで結論を出すことを防ぎます。

直前のローソク足と次のローソク足の相関

しかし、得られた結果を評価してみます。観測された相関は−0.018であり、ゼロに近い値ですが負の符号を持っています。

これは、隣接するバーの間に明確な関係がないことを意味します。さらに、このわずかな負の値は、弱い平均回帰の傾向を示唆しています。ある方向への動きの後、次のバーは反対方向に動く可能性がやや高いということです。しかし、その効果の大きさは非常に小さく、実務的には統計的ノイズの域を出ません。

移動の継続という仮説は支持されません。H1時間足における市場は、モメンタムシステムというよりもランダムプロセスに近い挙動を示します。これは重要な観察であり、ナイーブな戦略の一群を即座に排除し、より現実的な分析の出発点を与えます。

単一バー単位ではノイズが多すぎます。このようなデータは構造ではなくノイズに支配されます。そのため、次の論理的ステップとして観測を拡張し、個々の変化ではなく平均的な動きを確認します。

単一バーの代わりに、過去1〜23バーの価格変化の平均を取ります。これによりランダムな変動が平滑化され、動きのより安定した成分を抽出できます。同様に、将来の価格変化についても1〜9バーのホライズンで平均を計算します。これにより、点的な観測からより安定したシグナルへと移行します。

実装は2つのブロックに明確に分割されます。最初のブロックでは、過去の変化に対する移動平均を計算します。

# Add rolling mean features for the previous and future moves
for period in range(2, 24, 1):
    data_matrix[f'last_mean_{period:02d}'] = data_matrix['last'].rolling(window=period).mean()

2つ目は将来の変化に対するものであり、将来から過去への情報リークを防ぐためにシフトを必須とします。これは極めて重要なポイントであり、これがなければ分析は意味を持ちません。

for period in range(2, 10, 1):
    data_matrix[f'next_mean_{period}'] = data_matrix['next'].rolling(window=period).mean().shift(-(period-1))

計算後は、欠損値を含む行を削除します。これはウィンドウ処理に伴う必然的な結果です。

# Remove rows with missing values created by rolling calculations
data_matrix.dropna(inplace=True)

次に、Pearsonの相関行列が構築され、その中から必要な部分行列、つまり「過去から未来への依存関係」が抽出されます。ここで中心的な問いへの答えが得られます。平均的な価格変動に予測力はあるのか、という点です。

correlation_matrix = data_matrix.corr('pearson')
# Match columns that begin with "next"
reg = r'^next.*$'
selected_cols = correlation_matrix.filter(regex=reg).columns
remaining_rows = correlation_matrix.index.difference(selected_cols)
correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]

ここではSeabornによるヒートマップ形式の可視化が特に適しています。これにより、パラメータグリッド全体にわたる依存関係の構造を素早く評価することができます。

plt.figure(figsize=(12, 7))
plt.subplots_adjust(left=0.15, right=1, bottom=0.16, top=0.95)
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Means Last to Next Bars') 
plt.savefig('mean_to_bar.png')
plt.show()

方法論的な観点から見ると、これはすでにより成熟した実験です。私たちは単純なバーごとのテストから離れ、集約効果の分析へと移行しています。もし市場に弱いパターンが存在するのであれば、それはまさにこのレベルで現れ始めます。

一定期間の平均価格変化と将来の値動きの相関

得られた結果はより興味深いものに見えますが、全体的な結論は依然として慎重です。依然として負の相関値が観測されます。しかし今回はそれが構造化されています。行列の中央領域(過去8〜14バー、将来ホライズン5〜8バー)では効果が強まり、−0.02〜−0.03の値に達します。弱いながらも、一貫した平均回帰傾向が確認できます。

ロジックは比較的明確です。市場がある方向に一定期間動いた場合、その後の数バーで部分的な修正が発生する確率が高くなります。ただし、この効果は線形ではありません。

  • 短いウィンドウではノイズに埋もれる
  • 長すぎるウィンドウでは平滑化されすぎて効果が薄れる
  • 中間レンジで最大の効果が現れる

行列の右下領域も特に注目に値します。そこでは相関がゼロに近づき、一部では正の値さえ示します。これは過度な平滑化によって予測力が失われる典型的な兆候です。すなわち、データの変動性が失われると同時にシグナルも消えてしまいます。

Seabornによる可視化は、この依存関係の強弱を非常に明確に示します。全体像はかなり意味のあるものになります。。

結論はシンプルです。市場は強いモメンタムを示さないものの、弱い平均回帰の性質は持っています。これはそのまま使える戦略にはなりませんが、方向性を示すものではあります。

得られた相関値は小さいため、このシグナルをそのまま売買に利用することはできません。しかし、ここからがより興味深い段階です。単一の関係が弱いのであれば、特徴量の組み合わせによって強化することができます。つまりモデルへと移行します。

重要な課題は、情報量のある特徴空間を構築することです。単一の価格系列だけでは不十分です。そのため次のステップでは、市場の隠れた構造を捉えられる追加特徴を探索します。基本セットとしては古典的なテクニカル指標を用いるのが合理的です。これは長年使われてきた手法であり、単純でありながら有用なシグナルを提供することが多いです。

次に、同じ規律あるアプローチを適用します。すなわち相関による検証です。さまざまなインジケーターの値と将来の価格変動との関係を評価します。このときインジケーターのパラメータはループで変化させます。これにより広範な構成を一度に探索し、どこで最も強いシグナルが出るかを確認できます。

実務的には、このプロセスはパラメータの列挙とフィルタリングに似ています。しかし本質的には特徴空間の生成と選択です。ここでの相関は診断ツールとして機能します。どのデータ変換が将来の値動きと関連しているかを測定します。これは単なるパラメータ調整とは本質的に異なるアプローチです。まず構造を理解し、その後に利益を抽出するという流れです。

コード上では、このアプローチは体系的に実装されます。一方では、移動平均、その一次・二次差分、現在値からの乖離といった価格の単純な派生量が使用されます。これは局所的なダイナミクスや市場の加速度を捉えようとする試みです。

# Recreate the base matrix for indicator engineering
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
# Add 11-period previous move averages and derived momentum features
data_matrix[f'last_mean_11'] = data_matrix['last'].rolling(window=11).mean()
data_matrix[f'Dlast_mean_11'] = data_matrix[f'last_mean_11'].diff()
data_matrix[f'DDlast_mean_11'] = data_matrix[f'Dlast_mean_11'].diff()
# Feature representing the gap between the rolling mean and the current move
data_matrix[f'last_last_11'] = data_matrix[f'last_mean_11'] - data_matrix['last']
data_matrix[f'Dlast_last_11'] = data_matrix[f'last_last_11'].diff()
data_matrix[f'DDlast_last_11'] = data_matrix[f'Dlast_last_11'].diff()
# Add short-term future sum targets for the next bars
for period in range(2, 10, 1):
    data_matrix[f'next_{period}'] = data_matrix['next'].rolling(window=period).sum().shift(-(period-1))

一方で、TAライブラリの古典的な指標も追加されます:SMARSIMACD,です。さらに、それらは複数のパラメータ設定で同時に計算されます。このように網羅的に扱うことで、適切な期間を推測するのではなく、全体のレンジにわたる挙動を観察できるようになります。

# Build additional technical indicators using the close price series
close = pd.DataFrame(close[:-1], columns=['close'])
indicator_cols = {}
for period in [4, 8, 12, 24, 36, 48]:
    sma = ta.trend.sma_indicator(close['close'], window=period, fillna=True)
    dsma = sma.diff()
    ddsma = dsma.diff()
    rsi = ta.momentum.rsi(close['close'], window=period, fillna=True)
    drsi = rsi.diff()
    ddrsi = drsi.diff()
    macd = ta.trend.MACD(
        close['close'],
        window_slow=2 * period,
        window_fast=period,
        window_sign=period * 3 // 4,
        fillna=True,
    )
    macd_main = macd.macd()
    dmacd = macd_main.diff()
    ddmacd = dmacd.diff()
    macd_diff = macd.macd_diff()
    dmacd_diff = macd_diff.diff()
    ddmacd_diff = dmacd_diff.diff()
    macd_signal = macd.macd_signal()
    dmacd_signal = macd_signal.diff()
    ddmacd_signal = dmacd_signal.diff()
    macd_sig_main = macd_signal - macd_main
    dmacd_sig_main = macd_sig_main.diff()
    ddmacd_sig_main = dmacd_sig_main.diff()

    indicator_cols[f'SMA_{period:02d}'] = sma
    indicator_cols[f'DSMA_{period:02d}'] = dsma
    indicator_cols[f'DDSMA_{period:02d}'] = ddsma
    indicator_cols[f'RSI_{period:02d}'] = rsi
    indicator_cols[f'DRSI_{period:02d}'] = drsi
    indicator_cols[f'DDRSI_{period:02d}'] = ddrsi
    indicator_cols[f'MACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_main
    indicator_cols[f'DMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd
    indicator_cols[f'DDMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd
    indicator_cols[f'MACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_diff
    indicator_cols[f'DMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_diff
    indicator_cols[f'DDMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_diff
    indicator_cols[f'MACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_signal
    indicator_cols[f'DMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_signal
    indicator_cols[f'DDMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_signal
    indicator_cols[f'MACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_sig_main
    indicator_cols[f'DMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_sig_main
    indicator_cols[f'DDMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_sig_main

# Append all indicator columns to the feature matrix in one operation
# This avoids repeated DataFrame assignment and keeps the DataFrame compact
data_matrix = pd.concat([data_matrix, pd.DataFrame(indicator_cols)], axis=1)
# Remove any rows with NaN values created by indicator calculations
data_matrix.dropna(inplace=True)

特に示唆的なのは二次導関数の追加です(diffおよびdiffの差分)。これはシグナルそのものの変化を捉えようとする試みであり、市場の動きにおける加速と減速の分析へと移行することを意味します。金融時系列においては、このような効果はレベルそのものよりも有益であることが多くあります。

次にフィルタリングを適用します。相関行列全体の中から、目的変数との最大の関係が所定の閾値(この場合は0.02)を超える特徴量のみを残します。これは重要なポイントです。私たちは弱く不安定な依存関係を意図的に切り捨て、データの中に最低限でも一貫して存在するものだけを残します。

correlation_matrix = data_matrix.corr('pearson')
selected_cols = correlation_matrix.filter(regex=reg).columns
remaining_rows = correlation_matrix.index.difference(selected_cols)
correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]
# Delete rows with low correlations
correlation_matrix = correlation_matrix[correlation_matrix.abs().max(axis=1) >= 0.02]

Seabornによる可視化によって、この工程は完了します。

plt.figure(figsize=(12, 7))
plt.subplots_adjust(left=0.2, right=1, bottom=0.05, top=0.95)
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Indicators to Next Bars') 
plt.savefig('trend_to_bar.png')
plt.show()

ヒートマップはもはや無秩序なノイズには見えず、シグナルマップへと変化します。どのインジケーター群が将来の値動きに反応し始めているのか、その反応がどのパラメータで強まり、どこで完全に消失するのかが明確になります。

インジケーターと将来価格変動の相関

ここから仮説群に一定の体系性が見えてきます。私たちはインジケーターのランダムな選択から、特徴量の意図的な構築へと移行しています。これはまだモデルではなく、モデル構築の土台に過ぎません。次の課題は、これらの弱く分散したシグナルを一つのシステムに統合し、個別ではほとんど見えない安定した依存関係を抽出することです。


4. モデルの構築と学習

前のステップでは相関マップを取得し、正および負の相関が最大となる特徴量を選択しました。ここで重要なのは、負の相関も本質的には同じシグナルであり、符号が反転しているだけだという点です。モデルの観点から見れば、これは制約ではなく追加情報として扱われます。

次に学習へ移ります。Scikit-Learnのエコシステムは、線形モデルからアンサンブルまで幅広いアルゴリズムを提供しています。記事「Scikit-learnライブラリの回帰モデルとONNXへのエクスポート」では55種類の回帰モデルの比較が紹介されています。しかし実務的な問題を解く場合には、堅牢で実績のあるアルゴリズムに絞るのが合理的です。この場合はRandomForestRegressorを使用します。

古典的なランダムフォレストは、単純さと表現力のバランスを取った手法です。非線形依存関係に強く、ノイズに対してロバストであり、強いデータ正規化も必要としません。これはモデリングの初期段階として理想的な特性です。

次の重要なステップはハイパーパラメータの選定です。ここでは木の数(n_estimators)と木の深さ(max_depth)の2つのパラメータによる直接的な総当たり探索をおこないます。これは妥当な選択です。前者はアンサンブル全体の強度を決定し、後者は個々の木のデータへの適応度と過学習の制御を担います。

このために新しいスクリプトを作成します。MetaTrader 5への接続および過去データの読み込みアルゴリズムは同一であるため、ここでは説明を省略します。

過去データ取得後、特徴空間の構築が始まります。価格差分、その平滑化版、および派生特徴量の基本行列が構築されます。

macd_settings = [(8,16,6),(12,24,9),(36,72,27),(48,96,36)]
features = []

# Build the base feature matrix from close price changes
close = pd.DataFrame(rates_frame['close'][:-1].to_numpy(dtype=float), columns=['close'])
diff = rates_frame['close'].diff().to_numpy(dtype=float)
# Pair consecutive differences into 'last' and 'next' columns
diff = np.column_stack((diff[:-1], diff[1:]))
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
features.append('last') # Add to features list for later use
# Add a 11-period rolling mean of the previous bar move
data_matrix['last_11'] = data_matrix['last'].rolling(window=11).mean()
features.append('last_11') # Add to features list for later use 
# Add the difference between the rolling mean and current bar move
data_matrix['last_last_11'] = data_matrix['last_11'] - data_matrix['last']
features.append('last_last_11') # Add to features list for later use

次に、テクニカル指標が追加されます。SMAMACD構成のセットです。

# Add a 12-period simple moving average as a technical feature
data_matrix['SMA_12'] = ta.trend.sma_indicator(close['close'], window=12, fillna=True)
features.append('SMA_12') # Add to features list for later use

# Add MACD-based technical indicators for the selected parameter sets
for fast, slow, sign in macd_settings:
    macd = ta.trend.MACD(
        close['close'],
        window_slow=slow,
        window_fast=fast,
        window_sign=sign,
        fillna=True,
    )
    macd_main = macd.macd()
    dmacd = macd_main.diff()
    macd_signal = macd.macd_signal()
    dmacd_signal = macd_signal.diff()
    macd_sig_main = macd_signal - macd_main

    sufix = f"{fast:02d},{slow:02d},{sign:02d}"
    data_matrix[f'MACD_MAIN_{sufix}'] = macd_main
    features.append(f'MACD_MAIN_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_MAIN_{sufix}'] = dmacd
    features.append(f'DMACD_MAIN_{sufix}') # Add to features list for later use     
    data_matrix[f'MACD_SIGNAL_{sufix}'] = macd_signal
    features.append(f'MACD_SIGNAL_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_SIGNAL_{sufix}'] = dmacd_signal
    features.append(f'DMACD_SIGNAL_{sufix}') # Add to features list for later use
    data_matrix[f'MACD_Sig_Main_{sufix}'] = macd_sig_main
    features.append(f'MACD_Sig_Main_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_Sig_Main_{sufix}'] = macd_sig_main.diff()
    features.append(f'DMACD_Sig_Main_{sufix}') # Add to features list for later use

すべての特徴量は順次1つのデータ構造へ蓄積され、その名前は別個のfeaturesリストに保存されます。これにより後続の作業が簡潔になり、変数の偶発的な欠落も防ぐことができます。

同じ段階で、目的変数も構築されます。これは指定ホライズンにおける累積価格変動(next_9)です。

# Add a 9-period future return target for the next bars
data_matrix['next_9'] = data_matrix['next'].rolling(window=9).sum().shift(-8)

このようにして、問題は回帰問題として定式化されます。すなわち、現在のインジケーター群を基に将来の価格変動を予測します。

次のステップはデータクリーニングです。スライディングウィンドウや差分処理を適用すると、欠損値は必然的に発生します。これらはモデルを正しく学習させるために削除されます。

data_matrix.dropna(inplace=True)

その後、データは時系列順に分割されます。最初の90%を学習用、残り10%をテスト用として使用します。これは極めて重要なポイントです。通常の機械学習問題とは異なり、ここではデータのシャッフルは許されません。私たちは時系列の順序を厳密に保持し、「まず過去があり、その後に未来が来る」という現実のプロセスを再現します。

# ===== 1) Data preparation =====
# Copy the raw feature matrix (preserves original data for later reference)
df = data_matrix.copy()

# Keep only features that are actually present in the DataFrame
features = [c for c in features if c in data_matrix.columns]

df = df[features + ["next_9"]]
X = df[features]
y = df["next_9"]

# ===== 2) Time-based split =====
split_idx = int(len(X) * 0.9)

X_train = X.iloc[:split_idx]
X_test = X.iloc[split_idx:]
y_train = y.iloc[:split_idx]
y_test = y.iloc[split_idx:]

その後、RandomForestRegressorモデルのハイパーパラメータ探索ループが開始されます。各パラメータの組み合わせに対して、完全な処理サイクルが実行されます。

  • 学習用サンプルでモデルを学習します。
  • # ===== 3) Model =====
    results = []
    for est in range(60, 111, 5):
        for dep in range(2, 14, 1):
            print(f"\n=== Estimators: {est}, Max Depth: {dep} ===")
            model = RandomForestRegressor(
                n_estimators = est,
                max_depth = dep,
                max_leaf_nodes = None,
                min_samples_split = 6,
                min_samples_leaf = 3,
                bootstrap = True,
                random_state = 42,
                n_jobs = -1
                )
    
            model.fit(X_train, y_train)
  • 学習用およびテスト用データに対して予測を生成し、その結果を安定した形式へ変換します(NaNや無限大の処理を含む)。
  •         # ===== 4) Evaluation =====
            pred_train=np.nan_to_num(model.predict(X_train), nan=0.0, posinf=0.0, neginf=0.0)
            pred_test = np.nan_to_num(model.predict(X_test), nan=0.0, posinf=0.0, neginf=0.0)
  • 品質指標を計算します。
  •         pt_corr = np.corrcoef(pred_test, y_test)[0, 1]
            results.append((est, dep, pt_corr))
            print("Train R2:", round(r2_score(y_train, pred_train), 6))
            print("Test  R2:", round(r2_score(y_test, pred_test), 6))
            print("Test MAE:", round(mean_absolute_error(y_test, pred_test), 8))
            print("Pred/Target corr:", round(pt_corr, 6))
    

最も重要な指標は、テストサンプルにおける予測値と実測値の相関です。これは、モデルが価格変動の方向性をどれだけ捉えられているかを直接反映します。一方、MAEは補助的な指標として計算されます。これらは近似全体の品質や誤差レベルを管理するために使用されます。

すべての結果はテーブル形式で保存され、各行が特定のハイパーパラメータの組み合わせに対応します。その後、このテーブルは行列形式へ変換され、モデル品質とパラメータとの依存関係を可視化できるようになります。

# --- results -> DataFrame ---
df_results = pd.DataFrame(
    results,
    columns=["Estimators", "Max Depth", "Test Correlation"]
)

# --- pivot table ---
heatmap_data = df_results.pivot(
    index="Estimators",
    columns="Max Depth",
    values="Test Correlation"
).sort_index()

最後のステップは、Seabornによる可視化です。ヒートマップによって、どのパラメータ領域で最良の結果が得られるのかが明確に示されます。これにより、最適な構成を選択できるだけでなく、モデルの安定性も評価できます。つまり、パラメータをわずかに変更した際に、性能がどの程度変化するのかを確認できるのです。

# --- heatmap ---
plt.figure(figsize=(14, 10))
sns.heatmap(
    heatmap_data,
    annot=True,
    fmt=".4f",
    cmap="coolwarm",
    linewidths=0.5,
    cbar_kws={"label": "Test Correlation"}
)

plt.title("Heatmap of Test Correlation by n_estimators and max_leaf_nodes")
plt.xlabel("Max Nodes")
plt.ylabel("Estimators")
plt.tight_layout()
plt.show()

木の数と最大深度の最適な比率を見つける

ここで重要なポイントが浮かび上がります。モデルは無からシグナルを生み出すわけではありません。モデルがおこなうのは、先に発見された弱い依存関係を集約することだけです。分析段階で構造の兆候すら存在しなかったのであれば、どのようなモデルを用いても状況を改善することはできません。しかし、たとえ弱くてもシグナルが存在するのであれば、アンサンブルモデルはそれを増幅し、実用可能なレベルへ引き上げることができます。

ハイパーパラメータ探索の最初の反復のグラフでは、max_depth = 10の領域が明確に確認できます。この値は、依存関係を捉える能力と過学習の制御との間で、最も安定したバランスを示しています。実際、この領域でモデルは「単純すぎず、しかしノイズへの過学習も始まっていない」という動作モードに到達します。

その後、ロジックは自然に発展します。max_depthを決定した後、次の段階としてmax_leaf_nodesによる木構造の調整へ進みます。同時に、n_estimators(木の数)の探索範囲も絞り込み、以前に安定した結果が観測された領域のみを残します。これにより探索の解像度を高めることができます。探索ステップは小さくなり、本当に重要なパラメータ領域へ集中できるようになります。

このアプローチは、古典的な局所最適化手法に似ています。まず大まかな最大領域を特定し、その後に内部を丁寧に微調整していきます。その結果、明らかに弱い構成へ計算資源を浪費することなく、安定したパラメータの組み合わせへ迅速に到達できます。

コード上の変更は目的を限定したものです。探索範囲を明確化し、第2パラメータを変更します。一方で、スクリプト全体のアーキテクチャは維持されます。

フォレストのサイズとノード数の最適な比率を見つける

選定後は、最適なハイパーパラメータを固定し、モデルの最終学習へ進みます。構造的には大きな変更はありません。スクリプトから反復ループを取り除き、RandomForestRegressorの初期化時にパラメータ値を直接指定します。それ以外のロジック、つまり、データ準備、サンプル分割、学習、基本的な検証はそのまま維持されます。

次に、より繊細でありながら本質的なポイントが現れます。モデルはすべての予測に対して同じ確信度を持っているわけではありません。あるケースでは強いシグナルを出しますが、別のケースでは値がゼロ付近に留まり、実質的には不確実性の領域にあります。すべての予測を同等に扱ってしまうと、戦略は必然的にノイズを取引し始めます。

ここから自然な仮説が導かれます。弱いシグナルを無視し、モデルが十分な確信を示している場合、あるいは期待される値動きがコストを上回る場合のみを取引対象とする、という考え方です。これはすでに、単なる関数モデルから取引フィルタモデルへの移行を意味します。

コードでは、このアイデアが簡潔かつ無駄なく実装されています。まず、学習サンプルに基づいて予測値の絶対値に対する閾値を計算します。

# ===== 5) Simple PnL prototype =====
# Calculate strategy metrics for a vector of thresholds without an explicit loop
percentiles = np.arange(10, 100, 5)
thresholds = np.percentile(np.abs(pred_train), percentiles)

ここで重要なのは、閾値がtrainデータ上で決定され、それをtestデータへ適用している点です。これにより、実験の妥当性が保たれます。

次に、予測値と対応する閾値の行列を構築します。各レベルごとに、どのシグナルがフィルタを通過するかを示すマスクを計算します。ポジションは予測値の符号と全体相関の符号を組み合わせて決定され、これにより売買方向がモデルの性質と整合するようになります。

# Build a matrix where each column repeats the test predictions
pred_matrix = np.tile(pred_test[:, None], (1, thresholds.size))
threshold_matrix = thresholds[None, :]

# Generate a mask per threshold and compute sign positions
mask = np.abs(pred_matrix) >= threshold_matrix
position = np.sign(pred_matrix) * np.sign(pt_corr) * mask.astype(float)

その後、ポジションと実際の値動きの積から収益性を構築します。さらに固定コスト(スプレッド手数料)を差し引き、累積資本曲線を生成します。

# Broadcast y_check to match the threshold matrix shape
y_check_matrix = np.tile(y_check.values[:, None], (1, thresholds.size))
# Subtracting swap cost from the target to get a more realistic PnL estimate
strategy_ret = position * y_check_matrix - np.abs(position)*(0.00021)

# Compute equity curves for each threshold column
equity = np.cumsum(strategy_ret, axis=0)

結果はテーブルへ集約されます。

  • final_equity:最終収益
  • mean_return:平均トレード収益
  • win_rate:利益となったエントリーの割合
# Aggregate results into a DataFrame
results = pd.DataFrame({
    'percentile': percentiles,
    'threshold': thresholds,
    'final_equity': equity[-1, :],
    'mean_return': np.sum(strategy_ret, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9),
    'win_rate': np.sum(strategy_ret > 0, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9)
})

print(results.to_string(index=False, float_format='%.8f'))

実務的な観点から見ると、これはもはや単なるモデル評価ではなく、トレードシステムの原型です。私たちは予測精度を確認するだけでなく、その予測が異なるフィルタレベルでどのように収益化されるかまで同時に評価しています。


5.ONNXへの変換

モデルの学習が完了したので、次の論理的なステップは、人間を意思決定ループから外すことです。モデルシグナルに基づく手動トレードは、ほとんどの場合、自動売買に劣ります。継続性が失われ、反応速度も低下し、さらに心理的要因が加わるためです。実運用では、これはシステム的な歪みとして現れます。たとえば、エントリーの見逃し、早すぎる決済、自分自身のモデルへの不信などです。

MetaTrader 5プラットフォームには、主に2つの自動化手段があります。1つ目は、Pythonスクリプトを直接実行して注文を発行する方法です。2つ目は、モデルをONNX形式へ変換し、その後MQL5 EAで内で利用する方法です。実務的には、後者の方がより成熟したアプローチと言えます。

ONNX形式は複数の問題を同時に解決します。モデルはコンパクトかつ独立した形式で固定されます。必要なのはターミナルだけであり、コンピュータ間で容易に移植できます。また、ストラテジーテスターによる本格的な検証も可能になります。さらに、パフォーマンス面での損失もありません。ターミナルはGPU (CUDA)を含むハードウェアアクセラレーションをサポートしており、特にアンサンブルモデルを使用する場合に重要となります。

変換手順自体は比較的シンプルです。まず、モデル入力を定義します。つまり、特徴空間の次元数を指定します。

# Number of features used for model input
n_features = X_train.shape[1]

# Describe the model input shape for ONNX conversion
initial_type = [("float_input", FloatTensorType([None, n_features]))]

次に、Scikit-Learnで学習したモデルを適切なコンバータを通じてONNX形式へ変換し、ディスクへ保存します。

# Convert the trained sklearn model to ONNX format
onnx_model = convert_sklearn(model, initial_types=initial_type)

# Save the ONNX model to disk
with open(onnx_model_path, "wb") as f:
    f.write(onnx_model.SerializeToString())

その後、必須となる検証ステージへ進みます。ONNX Runtimeを用いてモデルを読み込み、同じデータを使用して予測値を計算します。

# Load the ONNX model for inference
sess = rt.InferenceSession(onnx_model_path)

input_name = sess.get_inputs()[0].name

# ONNX runtime expects float32 input arrays
X_test_np = X_test.astype(np.float32).values

onnx_preds = sess.run(None, {
    input_name: X_test_np
})[0].ravel()

続いて、それらの結果を元のSklearnモデルの予測と比較します。

# Compare ONNX predictions with sklearn predictions
sk_preds = model.predict(X_test)

print("Correlation:", np.corrcoef(sk_preds, onnx_preds)[0, 1])
print("Max diff:", np.max(np.abs(sk_preds - onnx_preds)))

ここで重要な基準は2つあります。

  • 予測間の相関が1に近づいていること
  • 最大誤差が無視できるレベルであること

これらの条件が満たされれば、変換は成功したとみなすことができ、モデルはトレードシステムへの統合準備が整ったことになります。

実務的な観点から見ると、これはPythonによる研究環境から実運用への最終的な移行です。モデルは単なる実験ではなくなり、インフラの一部となります。すなわち、自律的で、再現可能であり、検証および実運用に適した存在へと変わるのです。


6. ストラテジーテスターでのテスト

Python側での作業が完了した後、ロジックはMetaTrader 5の実行環境へ移行されます。ここでモデルは研究ツールではなくなり、取引アルゴリズムの一部となります。重要なのは、コード構造がすでに馴染みのある処理フロー「初期化 → データ準備 → 予測 → 売買判断」に従っていることです。

初期化はOnInitメソッド内でおこなわれます。この段階では、リソースからONNXモデルを読み込み、OnnxCreateFromBufferを通じてランタイム環境を生成します。

int OnInit()
  {
//---
   if(!Symb.Name("EURUSD_i"))
      return INIT_FAILED;
   Symb.Refresh();
//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      return INIT_FAILED;
//--- load models
   onnx = OnnxCreateFromBuffer(model, ONNX_DEFAULT);
   if(onnx == INVALID_HANDLE)
     {
      Print("OnnxCreateFromBuffer error ", GetLastError());
      return INIT_FAILED;
     }
   const ulong input_state[] = {1, Inputs.Size()};
   if(!OnnxSetInputShape(onnx, 0, input_state))
     {
      Print("OnnxSetInputShape error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }
   const ulong output_forecast[] = {1, Forecast.Size()};
   if(!OnnxSetOutputShape(onnx, 0, output_forecast))
     {
      Print("OnnxSetOutputShape error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }

次に、入力および出力形状を明示的に設定します。これは極めて重要です。モデルは厳密に固定された特徴量数を前提としているため、この段階でのミスは誤った推論結果につながります。

学習時と同じパラメータを用いたSMAインジケーターとMACDセットも並行して初期化されます。

//--- Indicators
   if(!ciSMA.Create(Symb.Name(), TimeFrame, 12, 0, MODE_SMA, PRICE_CLOSE))
     {
      Print("SMA create error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }
   ciSMA.BufferResize(2);
   for(uint i = 0; i < ciMACD.Size(); i++)
     {
      if(!ciMACD[i].Create(Symb.Name(), TimeFrame, int(macd_set[i, 0]),
                int(macd_set[i, 1]), int(macd_set[i, 2]), PRICE_CLOSE))
        {
         PrintFormat("MACD %d create error %d", i, GetLastError());
         OnnxRelease(onnx);
         return INIT_FAILED;
        }
      ciMACD[i].BufferResize(4);
     }
//---
   return(INIT_SUCCEEDED);
  }

ここでの本質的なポイントは、MQL5側の特徴量が、学習時にモデルへ入力された特徴量と完全に一致していなければならないという点です。わずかな不一致でも予測能力は破壊されます。

メインロジックはOnTickメソッド内に配置されますが、IsNewBarフィルタによって新規バー生成時のみ実行されます。これにより、モデルが毎ティック再計算されることを防ぎ、計算を時間足と同期させます。

void OnTick()
  {
//---
   if(!IsNewBar())
      return;

その後、現在保有しているポジション管理ブロックが続きます。ここでは方向ごとのボリュームおよび損益を単純集計します。これは既存ポジションを管理するために必要です。

   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   int total = PositionsTotal();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
     }

続いて、Inputs初期データベクトルが構築されます。実質的には、Python側でおこなった特徴量エンジニアリングをMQL5内で手動再現しています。

  • 基本特徴量:
  • //--- prepare input data
       ciSMA.Refresh();
       for(uint i = 0; i < ciMACD.Size(); i++)
          ciMACD[i].Refresh();
       if(!Rates.CopyRates(Symb.Name(), TimeFrame, COPY_RATES_CLOSE, 1, 12))
         {
          Print("CopyRates error ", GetLastError());
          return;
         }
       Inputs[0] = float(Rates[11] - Rates[10]);
       Inputs[1] = float(Rates[11] - Rates[0]) / 11;
       Inputs[2] = float(Inputs[1] - Inputs[0]);
    
  • SMA
  •    Inputs[3] = float(ciSMA.Main(1));
  • MACDブロックとその派生データ
  •    for(uint i = 0; i < ciMACD.Size(); i++)
         {
          Inputs[4 + i * 6] = float(ciMACD[i].Main(1));
          Inputs[5 + i * 6] = float(Inputs[4 + i * 6] - ciMACD[i].Main(2));
          Inputs[6 + i * 6] = float(ciMACD[i].Signal(1));
          Inputs[7 + i * 6] = float(Inputs[6 + i * 6] - ciMACD[i].Signal(2));
          Inputs[8 + i * 6] = Inputs[6 + i * 6] - Inputs[4 + i * 6];
          Inputs[9 + i * 6] = Inputs[7 + i * 6] - Inputs[5 + i * 6];
         }
    

ここで注目すべきなのはインデックス管理です。各特徴量は厳密に定義された位置を占有しています。これはモデルと実行環境との間の契約です。順序が崩れると、モデルは歪んだ入力に基づいて動作してしまいます。

特徴量準備後、OnnxRunが呼び出されます。

//--- run the inference
   if(!OnnxRun(onnx, ONNX_LOGLEVEL_INFO, Inputs, Forecast))
     {
      Print("OnnxRun error ", GetLastError());
      return;
     }

出力されるのは、期待される価格変動を表す予測値です。その後、実践的な部分であるシグナル解釈へ進みます。

コードでは、単純ながら効果的なロジックが採用されています。

  • 弱いシグナルを除外するための閾値を導入する(学習結果から判断)
  • 相関方向を考慮し、必要に応じてモデルを反転できるようにする
  • 予測が閾値を超えた場合のみポジションを開き、シグナルが消えた場合は決済する
   Symb.Refresh();
   Symb.RefreshRates();
   double min_lot = Symb.LotsMin();
   double step_lot = Symb.LotsStep();
   double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point();
//--- buy control
   if(Forecast[0]*direction >= threshold)
     {
      double buy_lot = min_lot;
      if(buy_value <= 0)
         Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), 0, 0);
     }
   else
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }
//--- sell control
   if(Forecast[0]*direction <= -threshold)
     {
      double sell_lot = min_lot;
      if(sell_value <= 0)
         Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), 0, 0);
     }
   else
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
  }

このように、モデルは方向性フィルタとして使用されます。ここが重要な違いです。私たちはすべての予測値を取引するのではなく、十分な強度を持つシグナルだけを対象にしています。

完成したEAは、その後MetaTrader 5ストラテジーテスター上で重要な検証を受けます。対象は2026年第1四半期の過去データです。これはもはや抽象的なモデル評価ではなく、現実に近い実装シナリオです。

このテスト形式は本質的に重要です。Python段階ではモデルをメトリクスで評価していましたが、ここではシステム全体が検証されます。特徴量生成からポジション管理ロジックまで、すべてが対象です。実質的に、戦略は初めて現実に近い条件下で試験を受けることになります。

テスト段階によって、開発サイクルは完成します。仮説とデータ分析から始まり、モデル構築、自動化、そして過去データによる検証へ至ります。ここで初めて、弱い統計的依存関係が実用的なトレードツールへ変換できたかどうかが明らかになります。

ただし、ONNXモデルをEAへ統合することだけが利用方法ではありません。裁量トレードを好むユーザー向けに、MetaTrader 5ではモデルをカスタムインジケーターへ直接組み込むことも可能です。この場合、モデルはトレーダーの代わりに意思決定を下すのではなく、分析ツールとして機能します。ユーザーが独自に解釈するためのシグナルを生成する役割を担います。

エンジニアリング的観点から見ると、違いはほとんどありません。ONNXモデル接続、入力データ準備、推論実行の仕組みはEA実装と完全に同一です。変わるのは適用先だけです。自動売買の代わりに、モデル結果をチャート上へ表示したり、意思決定時の追加フィルタとして利用したりします。

ランダムフォレストを使用したインジケーター

このアプローチには独自の利点があります。モデルシグナルを古典的な分析と柔軟に組み合わせることができ、アルゴリズムの完全性に対する要求も低減されます。モデルは唯一の判断主体ではなく、トレーダーを補助する存在となるのです。


結論

PythonMetaTrader 5を統合することで、コンセプトから実装までを一貫してカバーする、エンジニアリング的に検証されたトレード開発フレームワークが構築されます。本記事では、その流れを段階的に追ってきました。データ取得と分析から始まり、仮説検証、モデル構築、そして実際の実行環境への実装と検証に至るまでです。

このアプローチの最大の利点は、役割分担が明確である点にあります。Pythonは研究・分析部分を担当します。すなわち、データ処理、特徴量生成、統計分析、モデル学習です。一方、MetaTrader 5は実行基盤を提供します。市場データへのアクセス、ストラテジーテスト、そして取引インフラです。これは典型的な「研究環境-本番環境」の構成であり、それぞれの環境が本来の目的に沿って利用されます。

さらに、ONNX形式の採用は追加的な価値をもたらします。モデルはポータブルになり、開発環境に依存せず、ターミナルがインストールされた任意のデバイス上で実行可能になります。これにより、スケーリングが容易になり、検証速度が向上し、環境間の非互換性によるリスクも低減されます。

記事で使用されているプログラム

# 名前 種類 詳細
1 Experts\Integration\Integration.mq5 EA ターミナルでモデルをテストするためのEA
2 Indicators\Integration\Integration.mq5 インジケーター チャート上にシグナルを表示するためのインジケーター
3 Scripts\Integration\load_data.py スクリプト データ読み込みスクリプト
4 Scripts\Integration\look_model_param_rf.py スクリプト ハイパーパラメータ列挙スクリプト
5 Scripts\Integration\create_model_rf.py スクリプト モデルの学習とONNXへのエクスポートをおこなうためのスクリプト

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/22020

添付されたファイル |
MQL5.zip (13.99 KB)
最後のコメント | ディスカッションに移動 (2)
Denis Kirichenko
Denis Kirichenko | 17 4月 2026 において 11:17

アーカイブにある スクリプトファイルload_data.pyの中にそのような行があります:

#  Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD_i", mt5.TIMEFRAME_H1, utc_from, utc_to)

という行があります:

#  Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

些細なことですが、テスト時にすぐにはわかりませんでした......。

私はVSでpythonを使っています。デバッグは3.11でしかできません。
Gloria Diana
Gloria Diana | 4 5月 2026 において 16:55
このリソースに感謝する
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
決定論的振動型探索(DOS) 決定論的振動型探索(DOS)
決定論的振動型探索(DOS, Deterministic Oscillatory Search)アルゴリズムは、乱数を使用せずに勾配法と群知能アルゴリズムの利点を組み合わせた、革新的な大域最適化手法です。適応度の振動と勾配状態メカニズムによって、DOSは複雑な探索空間を決定論的に探索することができます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
マルコフ状態遷移行列に基づくニューラルネットワークを用いた自己学習型エキスパートアドバイザー マルコフ状態遷移行列に基づくニューラルネットワークを用いた自己学習型エキスパートアドバイザー
マルコフ状態遷移行列に基づくニューラルネットワークを用いた自己学習型EA。本記事では、ALGLIB MQL5ライブラリで開発した多層ニューラルネットワーク(MLP)とマルコフ連鎖を組み合わせた自己学習型EAについて解説します。マルコフ連鎖とニューラルネットワークをどのように統合し、FX予測へ応用できるのでしょうか。