コンテンツにスキップ

【移植#73】最適化9パスの勝率をCSVへ — OnTesterとフレームで取りこぼさない

EURUSD H1の最適化9パスについて、実pass ID、EMA期間、取引数、勝ち数、勝率を一覧表示したMT5実画面

この記事の3行まとめ

  • 各テスト終了時に TesterStatistics() から総取引数と勝ち取引数を取得し、勝率を計算します
  • FrameAdd() で各エージェントから送り、FrameNext() の実pass IDと FrameInputs() の入力値をCSVへ記録します
  • EURUSD H1の完全最適化9パスで、9行・9個のpass ID・9組の入力値・勝率式を専用MT5で照合しました

今回の原典は「各PASSテストの勝率」

投稿日順で次の題材は、2010年3月22日の記事です。

元記事「Optimization 時の各PASSテストでの勝率を知る。」を読む

原典の相談は、MT4の最適化結果には損益・PF・DDが並ぶのに、各パスの勝率を確認するには同じ条件をもう一度テストする必要があり、効率が悪いというものでした。

そこで原典は、テスト終了時刻をあらかじめ調べ、その時刻に達したら注文履歴を走査し、勝率と入力パラメーターをCSVへ追記しています。当時のMT4で「一度終えた計算を繰り返さない」ための実用的な工夫です。

一方、この方式には次の前提がありました。

  • テスト終了時刻を先に調べて、EAへ埋め込む
  • CSVの行番号と最適化のPASS番号が同じ順序になると期待する
  • ゼロ取引のときは、別途ゼロ除算を防ぐ
  • 計算済みキャッシュや途中エラーで行が抜けていないか、人が確認する

MT5にはテスト終了イベントと最適化フレームがあるため、今回は時刻判定を使わず、pass IDも行番号から推測しない構成へ置き換えました。

勝率は「勝ち取引数 ÷ 総取引数」

各パスの終了後に呼ばれる OnTester() で、TesterStatistics() を読みます。

MQL
const long trades = (long)TesterStatistics(STAT_TRADES);
const long wins   = (long)TesterStatistics(STAT_PROFIT_TRADES);
const long losses = (long)TesterStatistics(STAT_LOSS_TRADES);
const long breakeven = trades-wins-losses;

const bool valid = trades > 0 && breakeven >= 0;
const double win_rate = valid
                        ? 100.0*(double)wins/(double)trades
                        : -1.0;

今回の定義は次のとおりです。

Text Only
勝率(%) = 勝ち取引数 ÷ 全決済取引数 × 100

損益ゼロの取引は総取引数には含めますが、勝ちには数えません。勝ち + 負け が総取引数に届かない場合、その差を損益分岐取引として別列に残します。

取引が0件なら勝率は定義できないため、配布EAは -1 を返してそのパスを無効扱いにします。原典にも書かれていたゼロ除算を、計算前の条件で防いでいます。

勝率の分母を先に決める

売買システムによっては、損益ゼロを除外した 勝ち ÷ (勝ち + 負け) を勝率と呼ぶこともあります。今回はMT5レポートの全決済取引を分母にしました。別の定義と数値を比べるときは、分母が同じか確認してください。

各エージェントの結果をフレームで集める

最適化中の各パスはテストエージェント側で動きます。そこから端末側の収集処理へ、配列を直接共有することはできません。

配布EAは OnTester() で次の値を1つのフレームへまとめ、FrameAdd() で送ります。

  • スキーマ番号
  • 高速EMA期間、低速EMA期間
  • 総取引数、勝ち、負け、損益分岐
  • 勝率、純損益、PF、相対エクイティDD
  • パスが有効かどうか

OnTester() の戻り値にも勝率を返すため、テスターで最適化基準を Custom max にすれば、Custom列にも同じ値が入ります。

端末側では OnTesterPass() から FrameNext() を繰り返し、届いているフレームをすべて読みます。

MQL
while(FrameNext(pass,name,id,frame_value,payload))
  {
   // pass は、CSVの行番号ではなくテスターが付けた実pass ID
   // payloadと入力値を検証してから1行を書き出す
  }

フレームは複数まとめて届くことがあり、到着順もpass ID順とは限りません。さらに、最適化終了間際のフレームが OnTesterPass() に間に合わない場合があります。そのため、配布EAは OnTesterDeinit() でも同じ読み取り処理を行い、遅れて届いた分を回収してからCSVを閉じます。

行番号ではなく、実pass IDと入力値を照合する

CSVの先頭列へ書くのは、カウンターで作った連番ではありません。FrameNext() が返した実pass IDです。

さらに FrameInputs() から、そのpassで使われた入力値を取得します。フレーム内の高速・低速EMA期間と一致した行だけを正常な対応付けとして記録します。

実測でも、フレームは次の順でCSVへ届きました。

Text Only
0, 4, 1, 5, 2, 6, 3, 7, 8

4行目を「PASS 3」とみなす実装では、ここで対応が崩れます。到着順が変わっても、実pass IDと入力値をセットで保存すれば、後から正しく並べ替えられます。

9パスの実測結果

高速EMAを 8 / 10 / 12、低速EMAを 24 / 28 / 32 とし、3×3の全組み合わせを完全最適化しました。

到着順 pass ID Fast Slow 取引 勝ち 負け 勝率 純損益 PF
1 0 8 24 473 231 242 48.837% -298.53 0.955
2 4 10 28 473 228 245 48.203% -578.47 0.916
3 1 10 24 473 229 244 48.414% -628.97 0.908
4 5 12 28 473 228 245 48.203% -537.01 0.921
5 2 12 24 473 226 247 47.780% -660.71 0.904
6 6 8 32 473 229 244 48.414% -542.13 0.921
7 3 8 28 473 230 243 48.626% -582.67 0.915
8 7 10 32 473 230 243 48.626% -478.73 0.930
9 8 12 32 473 226 247 47.780% -541.51 0.921

損益分岐取引は全パス0件でした。たとえばpass 0は、231 ÷ 473 × 100 = 48.837209...% となり、CSVの勝率と一致します。

ここで最も大切なのは、最大勝率のpass 0でも純損益はマイナスという点です。勝率だけを最大化しても、平均利益が平均損失より小さければ利益は残りません。

専用MT5での検証

配布EAと、CSVを独立に読み直す表示用EAをコンパイルし、次の条件で確認しました。

項目 条件・結果
MT5 Build 6182
銘柄・時間足 EURUSD H1
期間 2025-01-01〜2025-12-31
ティック生成 Open prices only
初期証拠金・レバレッジ 10,000 USD・1:100
最適化 完全最適化、Custom max、9パス
配布EA コンパイル0エラー・0警告
表示用EA コンパイル0エラー・0警告
CSV 9行、実pass ID 9個、入力値の組み合わせ9個
数式検査 9行すべてで総数と勝敗数、勝率、Custom値が一致
レポート照合 9パスすべてでpass ID・入力値・取引数・Custom値が一致
テスターログ フレーム書込エラーなし
画像取得時の注文・ポジション 開始前後で変化なし

公開前の再検証でも、同じ最終コードから9パスすべてを受信しました。CSVの行数だけでなく、テスターレポートとログも同じ実行分で照合しています。

キャッシュ再生と新しい計算を区別する

同じEA・同じ条件の最適化では、テスターが計算済み結果を利用することがあります。集計処理を変更した直後の検証では、端末を停止してから対象EA・対象条件のキャッシュだけを整理し、新しいログとレポートで確認してください。広いフォルダーをまとめて削除する必要はありません。

配布EAのパラメーター

パラメーター 初期値 役割
InpFastPeriod 8 売買方向の判定に使う高速EMA期間
InpSlowPeriod 24 売買方向の判定に使う低速EMA期間
InpHoldBars 12 サンプル取引を保有するH1バー数
InpLots 0.10 サンプル取引のロット
InpMagic 730022 テスター内で自分のポジションを識別する番号
InpCsvFile 73_Optimization_Pass_WinRate.csv 出力CSV名。フォルダーを含まない名前だけを指定

このEMA売買は、勝率集計パイプラインを再現可能にするための測定用ロジックです。売買戦略としての優位性を主張するものではありません。実際に今回の9条件はすべて純損益がマイナスでした。

使い方

  1. 下のMQ5をMT5のデータフォルダー内の MQL5/Experts へ置き、MetaEditorでコンパイルします。
  2. ストラテジーテスターでEAを選び、最適化を有効にします。
  3. 集計したい入力の開始値・ステップ・終了値を設定します。
  4. OnTester() の勝率をCustom列へ出す場合は、最適化基準を Custom max にします。
  5. 全組み合わせが必要なら、遺伝的最適化ではなく完全最適化を選びます。
  6. 終了後、端末のFilesフォルダーに作られたCSVを表計算ソフトなどで開きます。

配布EAはストラテジーテスター専用です。通常チャートへ装着すると初期化を拒否し、ライブ注文は行いません。DLL・WebRequestも使いません。

73_Optimization_Pass_WinRate_Exporter_v1_00.mq5 をダウンロード

自分のEAへ組み込むとき

今回のサンプル売買をそのまま使う必要はありません。既存EAへ移す中心部分は、次の4点です。

  1. OnTester() で必要な統計値を読み、ゼロ取引を除外する
  2. passごとの値と識別用スキーマを FrameAdd() で送る
  3. OnTesterPass() と OnTesterDeinit() の両方から未読フレームを回収する
  4. FrameNext() のpass IDと FrameInputs() の入力値を一緒に保存する

同じEAがすでに OnTester() を独自の評価関数に使っている場合は、戻り値を勝率へ置き換える必要はありません。勝率はフレーム配列へ含め、戻り値は従来の評価関数のままにできます。

また、勝率で候補を絞る場合も、最低取引数、PF、DD、期間外データを同時に確認してください。10回中7勝と1,000回中700勝は同じ70%でも不確実性が異なり、勝率が高くても損大利小なら赤字になり得ます。

まとめ

原典の「最適化をやり直さず、各PASSの勝率を得たい」という目的を、MT5のテスターイベントと最適化フレームで組み直しました。

終了時刻ではなく OnTester()、CSVの行番号ではなく実pass ID、最後の通知だけでなく OnTesterDeinit() での残り回収を使うことで、並列に届く結果を追跡しやすくなります。

まずは自分のEAで勝率の分母を決め、ゼロ取引と最低取引数を明示したうえで、PF・DD・損益と一緒に比較してみてください。

あわせて読みたい記事


前後の記事

前: 【移植#72】必要な日だけPivotを引く — 前日値幅で描く4つのFiboZone

次: 【移植#74】再初期化しても同じ形か — 差分計算と全計算を500本照合

— SPONSORED —