ストラテジーテスターで最適化をかけたあと、
結果一覧をプロフィットファクターの数値順に並べ替えて、
上から目視で選んでいないでしょうか。
取引回数が数件しかない組み合わせや、
ドローダウンが致命的に大きい組み合わせが、
たまたま上位に紛れ込んでいることがあります。
この記事では、`OnTester()`の戻り値を「カスタム最適化基準」にして、
条件を満たさない組み合わせを自動で0点にする方法と、
`OnTesterPass()`・`FrameAdd`・`FrameNext`で
最適化の途中経過をログに出す方法を解説します。
・OnTester()がいつ呼ばれ、戻り値が何に使われるか
・OnTesterPass()とOnTester()の役割の違い
・TesterStatisticsで取引回数やDDを取得して足切りする方法
・FrameAdd/FrameNextで最適化の途中経過を監視する方法
OnTester()とは何か
まず、最適化の基本イベント関数であるOnTester()から確認します。
呼ばれるタイミングはOnDeinitの直前
`OnTester()`は、1回分のバックテストが終わるたびに、
`OnDeinit()`が呼ばれる直前に自動で呼び出されます。
最適化中は、パラメータの組み合わせごとに毎回呼ばれる計算です。
戻り値がカスタム最適化基準になる
double OnTester();
戻り値はdouble型です。
ストラテジーテスターの最適化基準で「カスタム最大」を選ぶと、
この戻り値が大きい組み合わせほど、良い結果として並び替えられます。
OnTesterPass()とOnTester()の違い
似た名前ですが、呼ばれる場所も役割も異なります。
フレームを受信した瞬間に呼ばれる
void OnTesterPass();
引数はありません。
`OnTesterPass()`は、最適化中のいずれかの組み合わせが完了し、
`FrameAdd()`でフレームが送信されるたびに呼び出されます。
`OnTester()`が「1回分の結果を計算する」役割なのに対し、
`OnTesterPass()`は「全体の進み具合を監視する」役割です。
MQL4との違い(OnTesterPassはMQL5専用)
`OnTester()`自体はMQL4にも存在しますが、
`OnTesterPass()`・`OnTesterInit()`・
`OnTesterDeinit()`は、
MQL4の公式イベント一覧には掲載されていません。
途中経過を監視する仕組みは、MQL5だけの機能です。
MQL4からMQL5への移行では、他にも構造体ベースのOrderSend等、
考え方ごと変わる部分が多くあります。
全体像は既刊のMQL4/MQL5比較記事も参考にしてください。
TesterStatisticsで取引回数やDDを取得する
戻り値を計算する前に、必要な統計値を取り出します。
TesterStatisticsのシグネチャ
double TesterStatistics(ENUM_STATISTICS statistic_id);
引数に統計項目のIDを渡すと、
そのテスト結果の値がdouble型で返ってきます。
| 定数 | 意味 |
|---|---|
| STAT_TRADES | 総取引回数 |
| STAT_PROFIT_FACTOR | プロフィットファクター(総利益÷総損失) |
| STAT_EQUITY_DD_RELATIVE | 最大資産減少率(%) |
取引回数・DDで足切りする設計思想
プロフィットファクターだけを最大化しようとすると、
「たった数回しか取引せず、たまたま勝った」組み合わせが、
上位に来てしまうことがあります。
このコードでは、最低取引回数とドローダウンの上限を
入力パラメータで決め、条件を満たさない組み合わせは、
戻り値を0にして最初から評価対象から外します。
FrameAdd/FrameNextで途中経過を監視する
最適化が終わるまで待たなくても、
良い結果が出た瞬間をログで確認できるようにします。
OnTester側でFrameAddを呼ぶ
bool FrameAdd(const string name, long id, double value, const void &data[]);
`OnTester()`の最後で、今回のスコアと詳細データを、
フレームとして送信しておきます。
OnTesterPass側でFrameNextを回す
bool FrameNext(ulong &pass, string &name, long &id, double &value, void &data[]);
`OnTesterPass()`の中で`FrameNext`をループさせると、
前回の呼び出し以降に届いた新しいフレームを、
1件ずつ順番に受け取れます。
実際に動くコード全体
移動平均線クロスの簡単なEAに、
カスタム最適化基準と途中経過ログを組み込んだ例です。
//+------------------------------------------------------------------+
//| MACrossOptimizable.mq5 |
//| Copyright 2026, FX-EA System Project Creator |
//| https://creator.fx-ea-system-project.com/ |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>
input int FastPeriod = 5;
input int SlowPeriod = 20;
input double LotSize = 0.1;
input int StopLossPoints = 300;
input int TakeProfitPoints = 600;
input int MinTrades = 30;
input double MaxDrawdownPercent = 30.0;
CTrade trade;
int fastHandle, slowHandle;
int OnInit()
{
fastHandle = iMA(_Symbol, _Period, FastPeriod, 0, MODE_SMA, PRICE_CLOSE);
slowHandle = iMA(_Symbol, _Period, SlowPeriod, 0, MODE_SMA, PRICE_CLOSE);
if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
IndicatorRelease(fastHandle);
IndicatorRelease(slowHandle);
}
void OnTick()
{
if(PositionSelect(_Symbol))
return;
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(fastHandle, 0, 0, 3, fast) < 3)
return;
if(CopyBuffer(slowHandle, 0, 0, 3, slow) < 3)
return;
double point = _Point;
if(_Digits == 3 || _Digits == 5)
point *= 10;
if(fast[2] <= slow[2] && fast[1] > slow[1])
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
trade.Buy(LotSize, _Symbol, price,
price - StopLossPoints * point,
price + TakeProfitPoints * point);
}
else if(fast[2] >= slow[2] && fast[1] < slow[1])
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
trade.Sell(LotSize, _Symbol, price,
price + StopLossPoints * point,
price - TakeProfitPoints * point);
}
}
double OnTester()
{
double trades = TesterStatistics(STAT_TRADES);
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double ddPercent = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
if(trades < MinTrades)
return(0.0);
if(ddPercent > MaxDrawdownPercent)
return(0.0);
double data[3];
data[0] = trades;
data[1] = profitFactor;
data[2] = ddPercent;
FrameAdd("ma_cross_result", 0, profitFactor, data);
return(profitFactor);
}
void OnTesterPass()
{
static double bestScore = 0.0;
ulong pass;
string name;
long id;
double value;
double data[];
while(FrameNext(pass, name, id, value, data))
{
if(value > bestScore)
{
bestScore = value;
PrintFormat("新しい最良スコア: pass=%d PF=%.2f 取引回数=%.0f DD=%.1f%%",
(int)pass, value, data[0], data[2]);
}
}
}
ストラテジーテスターでの最適化設定手順
- MetaEditorで新規Expert Advisor「MACrossOptimizable」を作成し、上のコードを貼り付けてコンパイルする
- ストラテジーテスターでこのEAを選択し、「最適化」にチェックを入れる
- FastPeriod・SlowPeriodをそれぞれ「開始・ステップ・終了」を指定して最適化対象にする
- 最適化基準のプルダウンから「カスタム最大」を選ぶ
- 最適化を開始し、「エキスパートログ」タブに新しい最良スコアの通知が流れることを確認する
よくあるエラーと注意点
最適化まわりで見落としやすいポイントをまとめます。
最適化結果が全部0点になる
`MinTrades`や`MaxDrawdownPercent`の条件が厳しすぎると、
ほとんどの組み合わせが足切りされて0点になります。
まずは緩めの値で試してから、少しずつ厳しくすると原因を切り分けやすくなります。
単体テストではOnTesterPassが呼ばれない
・OnTesterPass()はフレームを受信したときに呼ばれる仕組みのため
・最適化を伴わない単発のバックテストでは呼ばれません
・動作確認は必ず最適化モードで行ってください
プロフィットファクターが無限大になる場合
損失が1件も無い組み合わせでは、
プロフィットファクターの計算上、非常に大きな値や特殊な値になることがあります。
`MinTrades`で最低取引回数を確保しておくと、
このような極端な結果を実用上ある程度抑えられます。
よくある質問
OnTester・OnTesterPassについて、
よく検索される疑問を3つにまとめて回答します。
カスタム最適化基準には何を返すのが正解ですか?
正解は1つではありません。
プロフィットファクターだけでなく、
リカバリーファクターやシャープレシオなど、
自分が重視する指標を組み合わせて設計するのが一般的です。
この記事ではまず、取引回数とドローダウンで足切りしたうえで、
プロフィットファクターを返す設計にしています。
OnTesterとOnTesterPassはどちらもEAに書く必要がありますか?
両方とも同じEAのソースコード内に定義します。
`OnTester()`は各パスの結果を計算してフレームを送信する側、
`OnTesterPass()`はそのフレームを受け取って監視する側という、
役割分担になっています。
インジケーターでもOnTesterは使えますか?
`OnTester()`はExpert Advisor専用のイベント関数です。
カスタムインジケーターやスクリプトには実装できません。
【MQL5】OnTester・OnTesterPassで最適化結果を自動でふるいにかけるカスタム最適化基準の作り方:まとめ
`OnTester()`の戻り値をカスタム最適化基準にすれば、
取引回数が少ない組み合わせや、
ドローダウンが大きすぎる組み合わせを、
数値順に並べる前の段階で自動的に除外できます。
`OnTesterPass()`・`FrameAdd`・
`FrameNext`を組み合わせれば、
最適化がすべて終わるのを待たなくても、
途中経過をログで確認しながら進められます。
コメント