「勝率70%のEAです」と言われると強そうに見えますが、
1回の負けが9回分の利益を吹き飛ばす大きさなら、
そのEAは長期的には資金を減らしていきます。
勝率だけでは、1回あたりの勝ちと負けの「大きさ」が見えません。
この記事では、決済済みの注文履歴から勝ちトレードの合計・負けトレードの合計を分けて集計し、
プロフィットファクター(総利益÷総損失)と1トレードあたりの期待値を、
チャート上に自動表示するインジケーターを作ります。
・勝率とプロフィットファクターが別の指標である理由
・決済履歴から入出金操作を除外して正しく集計する方法
・プロフィットファクターと期待値の計算式
・実際に動くコード全体
設計思想: 勝率だけでは見えないもの
まず、プロフィットファクターという指標の位置づけを整理します。
勝率とプロフィットファクターの違い
| 指標 | 見ているもの | 弱点 |
|---|---|---|
| 勝率 | 勝ったトレードの件数の割合 | 1回あたりの勝ち負けの大きさが分からない |
| プロフィットファクター | 総利益÷総損失の比率 | 件数が少ないと数値がぶれやすい |
勝率30%でも、勝った時の利益が負けた時の損失の5倍あれば、
プロフィットファクターは1を上回り、トータルでは資金が増えます。
既刊win-rate.mdとの違い
既刊win-rate.mdは、
Binary Option想定でRSIの逆張りシグナルが出た、
直後のローソク足で勝敗をその場で判定する、
シミュレーション型の勝率計算機でした。
本記事は実際にOrderSendで発注し、
OrderCloseで決済された本物の注文履歴を集計する点が異なります。
既刊weekday-hour-filter-ea-mt4.mdとの違い
既刊weekday-hour-filter-ea-mt4.mdも、
OrdersHistoryTotalで決済履歴を走査していましたが、
目的は曜日別の勝率を集計してエントリーを制限するフィルターでした。
本記事は曜日で分けず、
全履歴を対象にプロフィットファクターという別の指標を算出する点が異なります。
決済履歴を1件ずつ読み取る
集計の土台になる、履歴の走査部分です。
OrdersHistoryTotal/OrderSelectで走査する
for(int i = 0; i < OrdersHistoryTotal(); i++)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
continue;
// 1件ずつの集計処理
}
OP_BALANCE/OP_CREDITを除外する
・決済履歴には、実際の売買(OP_BUY/OP_SELL)だけでなく
・口座への入出金操作も混在する
・入出金操作はOrderType()がOP_BALANCE(値6)や
・OP_CREDIT(値7)を返す
・これらを除外しないと、入金額が「利益」として
・出金額が「損失」として集計されてしまう
if(OrderType() != OP_BUY && OrderType() != OP_SELL) continue;
対象シンボル・集計期間のフィルター
if(InpThisSymbolOnly && OrderSymbol() != _Symbol) continue; if(InpLookbackDays > 0 && OrderCloseTime() < TimeCurrent() - InpLookbackDays * 86400) continue;
InpLookbackDaysを0にすれば全期間、
正の値を入れればその日数分だけ遡って集計します。
勝ちトレードと負けトレードを分けて集計する
1件ずつの損益を、勝ち側・負け側に振り分けます。
損益はOrderProfit+OrderSwap+OrderCommissionの合計
double netProfit = OrderProfit() + OrderSwap() + OrderCommission();
スワップと手数料を含めないと、実際の損益と数値がずれるため、必ず3つを合計します。
勝敗の振り分け
if(netProfit > 0)
{
grossProfit += netProfit;
winCount++;
}
else if(netProfit < 0)
{
grossLoss += MathAbs(netProfit);
lossCount++;
}
プロフィットファクターの計算と0除算対策
double profitFactor = 0; bool hasInfinitePF = (grossLoss == 0 && grossProfit > 0); if(grossLoss > 0) profitFactor = grossProfit / grossLoss;
負けトレードが1件も無いとgrossLossが0になり、割り算ができないため、
その場合は専用のフラグで「無限大(負けなし)」として扱います。
期待値と合わせてパネル表示する
集計した数値を、1つのラベルにまとめます。
1トレードあたりの期待値
int totalCount = winCount + lossCount; double expectancy = (totalCount > 0) ? (grossProfit - grossLoss) / totalCount : 0;
勝ち負けを含めた全トレードの合計損益を、トレード件数で割ることで、
「平均して1回あたりいくら残るか」を表します。
OBJ_LABELで複数行表示する
string pfText = hasInfinitePF ? "∞(負けなし)" : DoubleToString(profitFactor, 2);
double winRate = (totalCount > 0) ? (double)winCount / totalCount * 100 : 0;
string text = "対象トレード数: " + (string)totalCount + "\n" +
"勝率: " + DoubleToString(winRate, 1) + "%\n" +
"プロフィットファクター: " + pfText + "\n" +
"期待値(1トレードあたり): " + DoubleToString(expectancy, 2);
ObjectSetString(0, LabelName, OBJPROP_TEXT, text);
リアルタイム更新のタイミング
・決済履歴はティックが来なくても増える(他のEAや裁量トレードの決済で変わる)
・OnCalculateに加えてEventSetTimer(5)で5秒ごとにも再集計する
・履歴件数が多い口座では、頻繁すぎる再集計は負荷になるため間隔は調整可能にする
実際に動くコード全体
ここまでの要素をまとめた、実際にMetaEditorでコンパイルできるコードです。
//+------------------------------------------------------------------+
//| ProfitFactorCalculator.mq4 |
//| Copyright 2026, FX-EA System Project Creator |
//| https://creator.fx-ea-system-project.com/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, FX-EA System Project Creator"
#property link "https://creator.fx-ea-system-project.com/"
#property version "1.00"
#property strict
#property indicator_chart_window
#property indicator_buffers 0
input bool InpThisSymbolOnly = false; //現在のシンボルだけを対象にする
input int InpLookbackDays = 0; //集計期間(日数、0なら全履歴)
input int InpUpdateSeconds = 5; //再集計の間隔(秒)
string LabelName = "ProfitFactorPanel";
//+------------------------------------------------------------------+
//| 決済履歴を集計し、パネルを更新する |
//+------------------------------------------------------------------+
void UpdatePanel()
{
double grossProfit = 0;
double grossLoss = 0;
int winCount = 0;
int lossCount = 0;
for(int i = 0; i < OrdersHistoryTotal(); i++)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
continue;
if(OrderType() != OP_BUY && OrderType() != OP_SELL)
continue;
if(InpThisSymbolOnly && OrderSymbol() != _Symbol)
continue;
if(InpLookbackDays > 0 &&
OrderCloseTime() < TimeCurrent() - InpLookbackDays * 86400)
continue;
double netProfit = OrderProfit() + OrderSwap() + OrderCommission();
if(netProfit > 0)
{
grossProfit += netProfit;
winCount++;
}
else if(netProfit < 0)
{
grossLoss += MathAbs(netProfit);
lossCount++;
}
}
int totalCount = winCount + lossCount;
double winRate = (totalCount > 0) ? (double)winCount / totalCount * 100 : 0;
bool hasInfinitePF = (grossLoss == 0 && grossProfit > 0);
double profitFactor = (grossLoss > 0) ? grossProfit / grossLoss : 0;
string pfText = hasInfinitePF ? "∞(負けなし)" : DoubleToString(profitFactor, 2);
double expectancy = (totalCount > 0) ? (grossProfit - grossLoss) / totalCount : 0;
string text = "対象トレード数: " + (string)totalCount + "\n" +
"勝率: " + DoubleToString(winRate, 1) + "%\n" +
"プロフィットファクター: " + pfText + "\n" +
"期待値(1トレードあたり): " + DoubleToString(expectancy, 2);
ObjectSetString(0, LabelName, OBJPROP_TEXT, text);
}
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
ObjectCreate(0, LabelName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, LabelName, OBJPROP_CORNER, CORNER_LEFT_LOWER);
ObjectSetInteger(0, LabelName, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, LabelName, OBJPROP_YDISTANCE, 10);
EventSetTimer(InpUpdateSeconds);
UpdatePanel();
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
EventKillTimer();
ObjectDelete(0, LabelName);
}
void OnTimer()
{
UpdatePanel();
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
UpdatePanel();
return(rates_total);
}
//+------------------------------------------------------------------+
チャート左下に、トレード数・勝率・プロフィットファクター・期待値の4行が常時表示されます。
MetaEditorでのコンパイル・動作確認手順
実際に動かすまでの手順です。
- MetaEditorで新規インジケーター「ProfitFactorCalculator」を作成し、上のコードを貼り付けてコンパイルする
- 「0 error(s), 0 warning(s)」になることを確認する
- 決済履歴のあるデモ口座、またはストラテジーテスターで任意のEAを一度走らせた後のチャートに適用する
- 左下にトレード数・勝率・プロフィットファクター・期待値の4行が表示されることを確認する
- 決済履歴が0件の状態では、0除算せずに0%・0.00と表示されることを確認する
よくあるエラーと注意点
実装・運用でつまずきやすいポイントをまとめます。
OP_BALANCE/OP_CREDITを除外し忘れると数値が狂う
・入金操作はOrderProfit()に入金額が入り
・「大きな勝ちトレード」として誤集計される
・出金操作も同様に「大きな負けトレード」として誤集計される
・OP_BUY/OP_SELL以外を除外するチェックを
・外すと発生する
負けトレードが0件のときの表示
プロフィットファクターは定義上、総損失が0なら無限大になります。
このインジケーターは0除算を避けるため、
hasInfinitePFフラグで「∞(負けなし)」という、
文字列表示に切り替えています。
ターミナルの履歴表示範囲設定に依存する
OrdersHistoryTotal()が返す件数は、MT4ターミナルの「履歴」
タブで設定している表示期間(直近1ヶ月・3ヶ月・全期間等)に影響されます。
InpLookbackDaysを長く設定しても、ターミナル側の表示範囲がそれより短ければ、
それ以上は集計できません。
よくある質問
プロフィットファクター集計インジケーターについて、よくある疑問に回答します。
プロフィットファクターはいくつあれば良いですか?
一般的には1.0を上回れば総合的には利益が出ている状態とされますが、
スプレッドや手数料を考えると、実務上は1.2〜1.3以上を目標にする、
という考え方もあります。
断定的な基準ではなく、あくまで目安として参考にしてください。
このインジケーターは発注まで行いますか?
行いません。
既存の決済履歴を読み取って表示するだけで、注文は一切出しません。
複数の通貨ペアを横断して集計できますか?
できます。
InpThisSymbolOnlyをfalseにすれば、
口座全体の決済履歴(全通貨ペア)を対象に集計します。
【MQL4】決済履歴からプロフィットファクター(PF)を自動集計して表示するインジケーターの作り方(勝率だけでは見えない収益性):まとめ
この記事では、決済済みの注文履歴を1件ずつ読み取り、
勝ちトレードの合計と負けトレードの合計を分けて集計することで、
プロフィットファクターと1トレードあたりの期待値を、
自動表示するインジケーターを解説しました。
勝率だけでは分からない「1回あたりの勝ち負けの大きさ」を、
この指標で補うことができます。
OP_BALANCE/OP_CREDITという入出金操作を除外する手順は、
決済履歴を扱うすべての集計スクリプトで共通して必要になる注意点です。
コメント