OnTester関数の基本
OnTester()は、ストラテジーテスター(バックテスト)の終了時に自動的に呼び出されるイベント関数です。バックテスト完了後に独自の評価指標(カスタム最適化基準)を計算し、その結果を最適化プロセスに反映させることができます。
通常のバックテストでは、MetaTraderが用意するプロフィットファクターやシャープレシオなどの標準指標しか最適化基準に使えませんが、OnTester()を実装することで自分だけのオリジナル評価指標を最適化基準として使用できるようになります。
書式・引数・戻り値
double OnTester(void);
引数
引数はありません(void)。
戻り値
| 型 | 説明 |
|---|---|
| double | カスタム最適化基準として使用される値。ストラテジーテスターの最適化時に「Custom max」を選択すると、この戻り値が最大化される方向で最適化が行われます。 |
基本的な使い方
OnTester()はEA(エキスパートアドバイザー)内に記述します。ストラテジーテスターでバックテストが完了した直後、OnDeinit()が呼ばれる前に実行されます。
// OnTester()の基本構造
double OnTester()
{
// バックテスト結果を評価する処理
double customCriterion = 0.0;
// 何らかの計算を行う
customCriterion = 計算結果;
// 戻り値が「Custom max」最適化基準として使われる
return(customCriterion);
}
呼び出しタイミング
OnTester()が呼び出されるタイミングは以下の通りです。
- バックテスト終了後、最後のティック処理が完了した直後
- OnDeinit()の前に呼ばれる
- ストラテジーテスター内でのみ動作する(ライブ取引では呼ばれない)
- 最適化実行時は各パス(パラメータの組み合わせ)ごとに呼ばれる
呼び出し順序をまとめると以下のようになります。
// イベント関数の呼び出し順序(ストラテジーテスター)
// 1. OnInit() ← テスト開始時
// 2. OnTick() ← 各ティックごと(繰り返し)
// 3. OnTester() ← テスト終了後
// 4. OnDeinit() ← EA解放時
プログラム例1:基本的な損益ベースの評価指標
最もシンプルな例として、総利益と総損失の比率から独自のスコアを算出するOnTester()を実装します。
//+------------------------------------------------------------------+
//| プログラム例1:基本的な損益評価指標 |
//| 総利益÷総損失(プロフィットファクター的な指標)を返す |
//+------------------------------------------------------------------+
#property strict
// EA本体のパラメータ(例)
input int MAPeriod = 20; // 移動平均期間
input double LotSize = 0.1; // ロットサイズ
input int TakeProfit = 100; // 利確(ポイント)
input int StopLoss = 50; // 損切り(ポイント)
//+------------------------------------------------------------------+
//| 初期化関数 |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ティック処理(簡易的なMA戦略の例) |
//+------------------------------------------------------------------+
void OnTick()
{
// 既存ポジションがあれば何もしない
if(OrdersTotal() > 0) return;
double ma = iMA(NULL, 0, MAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
double prevClose = iClose(NULL, 0, 1);
double prevClose2 = iClose(NULL, 0, 2);
// 価格がMAを上抜けたら買い
if(prevClose2 < ma && prevClose > ma)
{
OrderSend(Symbol(), OP_BUY, LotSize, Ask, 3,
Ask - StopLoss * Point, Ask + TakeProfit * Point,
"MA Cross Buy", 0, 0, clrBlue);
}
// 価格がMAを下抜けたら売り
else if(prevClose2 > ma && prevClose < ma)
{
OrderSend(Symbol(), OP_SELL, LotSize, Bid, 3,
Bid + StopLoss * Point, Bid - TakeProfit * Point,
"MA Cross Sell", 0, 0, clrRed);
}
}
//+------------------------------------------------------------------+
//| バックテスト終了時に呼ばれるイベント関数 |
//+------------------------------------------------------------------+
double OnTester()
{
// TesterStatistics()でバックテスト結果の統計を取得
double totalProfit = TesterStatistics(STAT_GROSS_PROFIT); // 総利益
double totalLoss = TesterStatistics(STAT_GROSS_LOSS); // 総損失(負の値)
double totalTrades = TesterStatistics(STAT_TRADES); // 総取引回数
// 取引が一定回数以上ない場合は評価しない
if(totalTrades < 10)
{
Print("取引回数が少なすぎます: ", totalTrades, " 回");
return(0.0);
}
// 総損失が0の場合(全勝の場合)の処理
if(totalLoss == 0.0)
{
Print("全勝! 総利益: ", totalProfit);
return(totalProfit);
}
// プロフィットファクター(総利益÷総損失の絶対値)を計算
double profitFactor = totalProfit / MathAbs(totalLoss);
Print("カスタム評価値(PF): ", DoubleToStr(profitFactor, 2),
" | 取引回数: ", totalTrades,
" | 総利益: ", DoubleToStr(totalProfit, 2),
" | 総損失: ", DoubleToStr(totalLoss, 2));
// この値が「Custom max」最適化基準として使用される
return(profitFactor);
}
//+------------------------------------------------------------------+
//| 終了処理 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("EA終了 - 理由コード: ", reason);
}
この例ではTesterStatistics()関数を使ってバックテスト結果の統計データを取得しています。取引回数が少なすぎる場合は0を返すことで、信頼性の低い結果を排除しています。
プログラム例2:リカバリーファクターによる評価
リカバリーファクター(純利益÷最大ドローダウン)は、リスクに対してどれだけ効率的に利益を上げているかを示す重要な指標です。この値をカスタム最適化基準として使います。
//+------------------------------------------------------------------+
//| プログラム例2:リカバリーファクターで最適化 |
//| 純利益÷最大ドローダウンを評価指標として返す |
//+------------------------------------------------------------------+
#property strict
input int FastMA = 10; // 短期MA期間
input int SlowMA = 30; // 長期MA期間
input double Lots = 0.1; // ロットサイズ
//+------------------------------------------------------------------+
//| 初期化関数 |
//+------------------------------------------------------------------+
int OnInit()
{
// 短期MAが長期MAより大きくならないようにバリデーション
if(FastMA >= SlowMA)
{
Print("FastMAはSlowMAより小さくしてください");
return(INIT_PARAMETERS_INCORRECT);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ティック処理(ゴールデンクロス・デッドクロス戦略) |
//+------------------------------------------------------------------+
void OnTick()
{
if(OrdersTotal() > 0) return;
double fastCurr = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double fastPrev = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 2);
double slowCurr = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double slowPrev = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 2);
// ゴールデンクロス→買い
if(fastPrev <= slowPrev && fastCurr > slowCurr)
{
OrderSend(Symbol(), OP_BUY, Lots, Ask, 3, 0, 0, "GC Buy", 12345, 0, clrBlue);
}
// デッドクロス→売り
else if(fastPrev >= slowPrev && fastCurr < slowCurr)
{
OrderSend(Symbol(), OP_SELL, Lots, Bid, 3, 0, 0, "DC Sell", 12345, 0, clrRed);
}
}
//+------------------------------------------------------------------+
//| バックテスト終了時:リカバリーファクターを計算して返す |
//+------------------------------------------------------------------+
double OnTester()
{
// 各種統計値を取得
double netProfit = TesterStatistics(STAT_PROFIT); // 純利益
double maxDrawdown = TesterStatistics(STAT_EQUITY_DD); // 最大ドローダウン(金額)
double totalTrades = TesterStatistics(STAT_TRADES); // 総取引回数
double profitTrades = TesterStatistics(STAT_PROFIT_TRADES); // 勝ちトレード数
double expectedPayoff = TesterStatistics(STAT_EXPECTED_PAYOFF); // 期待利得
// ログに詳細を出力
Print("=== バックテスト結果サマリー ===");
Print("純利益: ", DoubleToStr(netProfit, 2));
Print("最大DD: ", DoubleToStr(maxDrawdown, 2));
Print("取引数: ", (int)totalTrades);
Print("勝率: ", DoubleToStr(profitTrades / MathMax(totalTrades, 1) * 100, 1), "%");
Print("期待利得: ", DoubleToStr(expectedPayoff, 2));
// 最低取引回数のフィルター
if(totalTrades < 30)
{
Print("取引回数不足(30回未満)→ 評価値0");
return(0.0);
}
// 最大ドローダウンが0または利益がマイナスの場合
if(maxDrawdown <= 0.0 || netProfit <= 0.0)
{
Print("利益なしまたはDD計算不可 → 評価値0");
return(0.0);
}
// リカバリーファクター = 純利益 ÷ 最大ドローダウン
double recoveryFactor = netProfit / maxDrawdown;
Print("リカバリーファクター: ", DoubleToStr(recoveryFactor, 3));
return(recoveryFactor);
}
//+------------------------------------------------------------------+
//| 終了処理 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
リカバリーファクターが高いほど、少ないリスク(ドローダウン)で大きな利益を上げていることを意味します。最適化時に「Custom max」を選択すれば、この値が最大化されるパラメータの組み合わせが探索されます。
プログラム例3:複合スコアによる多角的評価
実際のトレーディングでは、単一の指標だけでなく複数の指標を組み合わせた総合スコアで評価するのが効果的です。この例では、プロフィットファクター・勝率・取引回数・ドローダウン率を組み合わせた複合スコアを計算します。
//+------------------------------------------------------------------+
//| プログラム例3:複合スコアによる多角的評価 |
//| 複数の評価指標を重み付けして総合スコアを算出 |
//+------------------------------------------------------------------+
#property strict
input int RSIPeriod = 14; // RSI期間
input int RSIBuyLv = 30; // RSI買いレベル
input int RSISellLv = 70; // RSI売りレベル
input double Lots = 0.1; // ロットサイズ
input int SL_Points = 200; // 損切り幅(ポイント)
input int TP_Points = 300; // 利確幅(ポイント)
//+------------------------------------------------------------------+
//| 初期化 |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ティック処理(RSI戦略の例) |
//+------------------------------------------------------------------+
void OnTick()
{
// 既にポジションがあれば何もしない
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS) && OrderSymbol() == Symbol())
return;
}
double rsiCurr = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 1);
double rsiPrev = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 2);
// RSIが売られすぎ水準から回復→買い
if(rsiPrev < RSIBuyLv && rsiCurr >= RSIBuyLv)
{
double sl = Ask - SL_Points * Point;
double tp = Ask + TP_Points * Point;
OrderSend(Symbol(), OP_BUY, Lots, Ask, 3, sl, tp, "RSI Buy", 0, 0, clrBlue);
}
// RSIが買われすぎ水準から下落→売り
else if(rsiPrev > RSISellLv && rsiCurr <= RSISellLv)
{
double sl = Bid + SL_Points * Point;
double tp = Bid - TP_Points * Point;
OrderSend(Symbol(), OP_SELL, Lots, Bid, 3, sl, tp, "RSI Sell", 0, 0, clrRed);
}
}
//+------------------------------------------------------------------+
//| 複合スコアを計算する関数 |
//+------------------------------------------------------------------+
double CalculateCompositeScore()
{
// ---- 統計データの取得 ----
double netProfit = TesterStatistics(STAT_PROFIT);
double grossProfit = TesterStatistics(STAT_GROSS_PROFIT);
double grossLoss = TesterStatistics(STAT_GROSS_LOSS); // 負の値
double totalTrades = TesterStatistics(STAT_TRADES);
double profitTrades = TesterStatistics(STAT_PROFIT_TRADES);
double maxDDPercent = TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // 最大DD(%)
double maxDD = TesterStatistics(STAT_EQUITY_DD);
double sharpRatio = TesterStatistics(STAT_SHARPE_RATIO);
// ---- フィルター条件 ----
// 取引回数が少なすぎる結果を除外
if(totalTrades < 20)
{
Print("フィルター: 取引回数不足 (", (int)totalTrades, " < 20)");
return(-1000.0); // 大きな負の値で排除
}
// 純利益がマイナスの結果を除外
if(netProfit <= 0)
{
Print("フィルター: 純利益がマイナス (", DoubleToStr(netProfit, 2), ")");
return(-500.0);
}
// 最大ドローダウンが大きすぎる結果を除外(30%超)
if(maxDDPercent > 30.0)
{
Print("フィルター: DD過大 (", DoubleToStr(maxDDPercent, 1), "% > 30%)");
return(-100.0);
}
// ---- 各指標のスコア計算 ----
// 1. プロフィットファクター(0〜10にスケーリング)
double pf = 0.0;
if(MathAbs(grossLoss) > 0)
pf = grossProfit / MathAbs(grossLoss);
double pfScore = MathMin(pf, 5.0) * 2.0; // PF=5以上は全て10点
// 2. 勝率スコア(0〜10にスケーリング)
double winRate = profitTrades / totalTrades * 100.0;
double winRateScore = MathMin(winRate, 100.0) / 10.0; // 勝率100%で10点
// 3. 取引回数スコア(多いほど信頼性が高い)
// 100回以上で満点(10点)
double tradeCountScore = MathMin(totalTrades / 100.0, 1.0) * 10.0;
// 4. ドローダウン抑制スコア(DDが小さいほど高評価)
// DD 0%→10点、DD 30%→0点
double ddScore = MathMax(0.0, (30.0 - maxDDPercent) / 30.0 * 10.0);
// ---- 重み付けによる総合スコア ----
double weightPF = 0.35; // プロフィットファクター重視
double weightWinRate = 0.20; // 勝率
double weightTrades = 0.15; // 取引回数(信頼性)
double weightDD = 0.30; // ドローダウン抑制
double compositeScore = pfScore * weightPF
+ winRateScore * weightWinRate
+ tradeCountScore * weightTrades
+ ddScore * weightDD;
// ---- 結果のログ出力 ----
Print("=== 複合スコア詳細 ===");
Print(" PFスコア: ", DoubleToStr(pfScore, 2), " (PF=", DoubleToStr(pf, 2), ")");
Print(" 勝率スコア: ", DoubleToStr(winRateScore, 2), " (勝率=", DoubleToStr(winRate, 1), "%)");
Print(" 取引数スコア: ", DoubleToStr(tradeCountScore, 2), " (取引数=", (int)totalTrades, ")");
Print(" DDスコア: ", DoubleToStr(ddScore, 2), " (DD=", DoubleToStr(maxDDPercent, 1), "%)");
Print(" ★総合スコア: ", DoubleToStr(compositeScore, 3));
return(compositeScore);
}
//+------------------------------------------------------------------+
//| バックテスト終了時のイベント |
//+------------------------------------------------------------------+
double OnTester()
{
return(CalculateCompositeScore());
}
//+------------------------------------------------------------------+
//| 終了処理 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
この例のポイントは以下の通りです。
- フィルター機能:取引回数が少ない、利益がマイナス、ドローダウンが大きすぎる結果を大きな負の値で排除
- スケーリング:各指標を0〜10のスコアに正規化してから合算
- 重み付け:プロフィットファクターとドローダウンを重視した設計
プログラム例4:取引履歴を直接分析してカスタム指標を算出
TesterStatistics()だけでなく、決済済み注文の履歴を直接走査して、連続損失回数や最大連敗額など、より細かな分析を行う例です。
//+------------------------------------------------------------------+
//| プログラム例4:取引履歴を直接分析する高度な評価 |
//| 連続損失・利益の安定性を評価指標に組み込む |
//+------------------------------------------------------------------+
#property strict
input int Period1 = 12;
input int Period2 = 26;
input double LotSize = 0.1;
//+------------------------------------------------------------------+
//| 初期化 |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| ティック処理(省略:任意の売買ロジックを実装) |
//+------------------------------------------------------------------+
void OnTick()
{
// ここには任意のトレードロジックを実装
// (本例ではOnTester()の解説に焦点を当てるため省略)
}
//+------------------------------------------------------------------+
//| バックテスト終了時:取引履歴を直接分析 |
//+------------------------------------------------------------------+
double OnTester()
{
// 決済済み注文の履歴を分析
int totalOrders = OrdersHistoryTotal();
if(totalOrders == 0)
{
Print("取引履歴なし");
return(0.0);
}
// ---- 分析用変数の初期化 ----
double profits[]; // 各取引の損益を格納する配列
int tradeCount = 0; // 有効な取引数
int maxConsecLoss = 0; // 最大連敗数
int currentConsecLoss = 0; // 現在の連敗カウント
double maxConsecLossAmount = 0.0; // 最大連敗時の損失額
double currentConsecLossAmount = 0.0;
double totalProfit = 0.0; // 総利益
// 配列サイズを事前確保
ArrayResize(profits, totalOrders);
// ---- 取引履歴をスキャンして損益データを収集 ----
for(int i = 0; i < totalOrders; i++)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
continue;
// 対象シンボルの決済注文のみ処理
if(OrderSymbol() != Symbol())
continue;
// OP_BUYまたはOP_SELLのみ(入金・出金等を除外)
int type = OrderType();
if(type != OP_BUY && type != OP_SELL)
continue;
// 損益(手数料・スワップ込み)
double orderProfit = OrderProfit() + OrderCommission() + OrderSwap();
profits[tradeCount] = orderProfit;
totalProfit += orderProfit;
tradeCount++;
// 連敗トラッキング
if(orderProfit < 0)
{
currentConsecLoss++;
currentConsecLossAmount += MathAbs(orderProfit);
if(currentConsecLoss > maxConsecLoss)
{
maxConsecLoss = currentConsecLoss;
maxConsecLossAmount = currentConsecLossAmount;
}
}
else
{
// 勝ちトレードで連敗リセット
currentConsecLoss = 0;
currentConsecLossAmount = 0.0;
}
}
// 有効な取引がない場合
if(tradeCount < 10)
{
Print("有効取引数不足: ", tradeCount);
return(0.0);
}
// 配列サイズを実際の取引数にリサイズ
ArrayResize(profits, tradeCount);
// ---- 利益の安定性(標準偏差)を計算 ----
double meanProfit = totalProfit / tradeCount;
double sumSqDiff = 0.0;
for(int j = 0; j < tradeCount; j++)
{
double diff = profits[j] - meanProfit;
sumSqDiff += diff * diff;
}
double stdDev = MathSqrt(sumSqDiff / tradeCount);
// ---- カスタムシャープレシオ的な指標 ----
// 平均利益 ÷ 標準偏差(大きいほど安定して利益を出している)
double customSharpe = 0.0;
if(stdDev > 0)
customSharpe = meanProfit / stdDev;
// ---- 連敗ペナルティを適用 ----
// 最大連敗が多いほどペナルティを与える
double consecLossPenalty = 1.0;
if(maxConsecLoss > 5)
consecLossPenalty = 5.0 / maxConsecLoss; // 5連敗超でペナルティ
// ---- 最終スコアの計算 ----
// カスタムシャープ × 連敗ペナルティ × √取引回数(信頼性補正)
double finalScore = customSharpe * consecLossPenalty * MathSqrt(tradeCount);
// ---- 詳細ログ出力 ----
Print("=== 取引履歴分析結果 ===");
Print("有効取引数: ", tradeCount);
Print("総損益: ", DoubleToStr(totalProfit, 2));
Print("平均損益: ", DoubleToStr(meanProfit, 2));
Print("損益の標準偏差: ", DoubleToStr(stdDev, 2));
Print("カスタムシャープ: ", DoubleToStr(customSharpe, 4));
Print("最大連敗数: ", maxConsecLoss, " (損失額: ", DoubleToStr(maxConsecLossAmount, 2), ")");
Print("連敗ペナルティ: ", DoubleToStr(consecLossPenalty, 2));
Print("★最終スコア: ", DoubleToStr(finalScore, 4));
return(finalScore);
}
//+------------------------------------------------------------------+
//| 終了処理 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
この例では、OrdersHistoryTotal()とOrderSelect()を使って決済済み注文を1件ずつ走査しています。TesterStatistics()では取得できない連敗数や損益の標準偏差といった情報を、取引履歴から直接計算している点がポイントです。
TesterStatistics()で取得できる主な統計定数
OnTester()内で使用するTesterStatistics()関数で取得できる代表的な統計定数を一覧にまとめます。
| 定数名 | 説明 |
|---|---|
| STAT_PROFIT | 純利益(総利益 - 総損失) |
| STAT_GROSS_PROFIT | 総利益 |
| STAT_GROSS_LOSS | 総損失(負の値) |
| STAT_TRADES | 総取引回数 |
| STAT_PROFIT_TRADES | 勝ちトレード数 |
| STAT_LOSS_TRADES | 負けトレード数 |
| STAT_EXPECTED_PAYOFF | 期待利得(1トレードあたりの平均損益) |
| STAT_EQUITY_DD | 最大ドローダウン(金額) |
| STAT_EQUITY_DDREL_PERCENT | 最大ドローダウン(%) |
| STAT_SHARPE_RATIO | シャープレシオ |
| STAT_PROFIT_FACTOR | プロフィットファクター |
| STAT_RECOVERY_FACTOR | リカバリーファクター |
最適化での使い方
OnTester()の戻り値をカスタム最適化基準として使用するには、ストラテジーテスターの最適化設定で以下の手順を行います。
- ストラテジーテスターを開き、EAを選択する
- 「最適化」にチェックを入れる
- 最適化基準のドロップダウンから「Custom max」を選択する
- 最適化するパラメータの範囲を設定する
- 「スタート」を押して最適化を実行する
「Custom max」を選択すると、OnTester()の戻り値が最大化される方向でパラメータの最適な組み合わせが探索されます。逆に最小化したい場合は、戻り値の符号を反転させる(負の値にする)ことで対応できます。
注意点・よくある間違い
- ライブ取引では呼ばれない:OnTester()はストラテジーテスター内でのみ動作します。ライブ取引や通常のチャートにEAをアタッチした場合は呼ばれません。
- 戻り値の型はdouble:int型ではなくdouble型を返す必要があります。整数値を返しても問題はありませんが、関数の宣言はdoubleにしてください。
- TesterStatistics()はOnTester()内でのみ有効:この関数はバックテスト終了後にしか正確な値を返しません。OnTick()内で呼んでも意味のある値は取得できません。
- 最適化時のパフォーマンス:OnTester()内で重い計算を行うと、最適化全体の速度に影響します。特に数千パスの最適化を行う場合は、計算量に注意してください。
- 取引回数のフィルターは必須:取引回数が極端に少ない結果は統計的に信頼できません。必ず最低取引回数のチェックを入れてから評価値を計算しましょう。


