「今日は絶対に増やさない」と決めていたはずなのに、含み損を抱えた瞬間に取り返そうとしてまたエントリーし、
気づけば1日に10回以上ポジションを持っていた——このパターンに心当たりがある人は多いはずです。
オーバートレード(過剰な取引回数)は、1回1回のエントリー判断そのものよりも、
「連続で負けたときに冷静さを失って回数だけが増えていく」というメンタル面の崩れが引き金になることがほとんどです。
この記事では、複雑な判定ロジックを一切使わず、「当日の新規エントリー回数が上限に達したら、
その日はそれ以上エントリーしない」というシンプルなガード機構だけを実装するEAのアイデアを、
設計の考え方と実際に動くコードで解説します。
エントリーの是非を判断するシグナル自体はこの記事の主題ではありません。
あくまで「何回目のエントリーからは強制的に止める」という、シグナルの手前に置く歯止めの作り方に焦点を当てます。
なぜ「回数を数えて止めるだけ」というシンプルな形にしたのか
オーバートレード対策というと、連敗時にロットを下げる、含み損の合計額で止める、
といった凝った資金管理ロジックを思い浮かべがちです。
ですが、これらは「今の含み損がいくらか」
「連敗が何回続いているか」を正確に把握する処理が必要になり、
実装も検証も一段階複雑になります。
このEAのアイデアは、あえて判断材料を「回数」の1つだけに絞ります。
理由は2つです。
- 数える対象が単純で、判定にブレが生じない: 損益額や連敗数は「含み損をどの時点の評価額で見るか」「スワップを含めるか」といった細かい仕様判断が発生します。回数は「今日、新規エントリーが何回発生したか」を数えるだけなので、仕様が揺れません
- 感情が入り込む余地を先に断つ: オーバートレードの本質は「もう1回だけ」という判断が繰り返されることです。損益額での歯止めは「もう少し待てば戻るかもしれない」という期待で無視されやすい一方、「本日の上限回数に達しました」という単純な事実は言い訳が効きにくく、機械的に止まります
このEAは新規エントリーのシグナル自体には一切関与しません。
既存のEA(このサイトのゴールデンクロス・デッドクロスEA等)のOnTick()の先頭に、
後述するガード処理を1つ足すだけで組み込めるように設計しています。
当日の取引回数をどう数えるか
「今日、何回エントリーしたか」を数えるには、次の2つの注文プールの両方を確認する必要があります。
- 保有中のポジション(MODE_TRADES): `OrdersTotal()`でループし、`OrderSelect(i, SELECT_BY_POS, MODE_TRADES)`で1件ずつ取得します。まだ決済されていない、今日開いたポジションがここに含まれます
- 決済済みの履歴(MODE_HISTORY): `OrdersHistoryTotal()`でループし、`OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)`で1件ずつ取得します。今日すでにエントリーして、すでに決済まで終わっているものがここに含まれます
保有中のポジションだけを数えると、「今日3回エントリーしたが、
そのうち2回はすでに利確・損切りで決済済み」というケースを見落とし、
実際には上限を超えているのに「今のポジションは1つだけだから、
まだ2回エントリーできる」と誤判定してしまいます。
オーバートレード対策としては、決済済みかどうかに関わらず「今日、
新規に注文を送った回数そのもの」を数える必要があるため、両方のプールを合算します。
それぞれのループでは、次の条件でフィルタリングします。
- `OrderSymbol() == Symbol()`: 他の通貨ペアの注文を混同しない
- `OrderMagicNumber() == MagicNumber`: 他のEAや裁量トレードの注文を混同しない
- `OrderType() == OP_BUY || OrderType() == OP_SELL`: 成行の買い・売りだけを数える。指値・逆指値(OP_BUYLIMIT等、値は2〜5)はまだ約定していない予約に過ぎないため除外し、入出金(OP_BALANCE/OP_CREDIT、値は6・7)も注文ではないため除外する
- `OrderOpenTime()`が「今日」の範囲内か
日付が変わったときの扱い: 「リセット処理」をあえて書かない
このEAには、日付が変わったら回数をゼロに戻す「リセット処理」
に相当するコードがありません。
理由は、回数を保持する変数(グローバル変数やstatic変数)を使わず、
その都度、注文履歴から「今日開いたものだけ」を数え直しているためです。
もし「取引のたびにカウンター変数を+1し、日付が変わったらカウンターを0に戻す」
という作りにした場合、次のような事故が起こり得ます。
- EAをチャートから一度外して、また付け直した瞬間にカウンターが0にリセットされ、今日すでに3回エントリーしていたのに、また上限まで新規エントリーできてしまう
- MT4自体を再起動した場合も同様に、メモリ上のカウンター変数は消える
- 「日付が変わったかどうか」の判定を`OnTick()`の中でTimeCurrent()を見て比較する処理を自分で書く必要があり、判定漏れがあると1日1回だけ発生するバグとして発見が遅れる
これに対して、「毎回、注文履歴を数え直す」設計であれば、EAやターミナルの再起動があっても、
正しい実績(今日すでに何回エントリーしたか)がそのまま反映されます。
リセット処理という「壊れる可能性のあるコード」自体を書かずに済む、という考え方です。
「今日」の境界(当日0時)は、次のように`TimeCurrent()`を日付文字列に変換してから戻すことで求めます。
datetime GetTodayStart()
{
return(StrToTime(TimeToStr(TimeCurrent(), TIME_DATE)));
}
`TimeToStr(TimeCurrent(), TIME_DATE)`は現在のサーバー時刻を「2026.09.10」
のような日付だけの文字列に変換し、`StrToTime()`でその文字列を再度datetime型に戻すと、
時刻部分が00:00:00になった「今日の始まり」の時刻が得られます。
日足(D1)のローソク足データから`iTime(NULL, PERIOD_D1, 0)`で求める方法もありますが、
その場合はチャートにD1の履歴データが読み込まれている必要があり、
履歴不足の環境では0が返るリスクがあります。
TimeToStr/StrToTimeの方法はチャートの時間足や履歴データの有無に関係なく常に計算できるため、
こちらを採用しています。
実際に動くコード全体
ここまでの内容をまとめた、当日のエントリー回数が上限に達したら新規注文を送らないガードEAです。
実際のエントリーシグナルはこの記事の主題ではないため、`TryEntry()`関数の中身は「新しい足が始まった瞬間に成行で1回注文を送る」
という最小限のダミー条件にしています。
実際に使う場合は、`TryEntry()`の中身を、お手持ちのエントリーロジック(移動平均線のクロス等)に差し替えてください。
//+------------------------------------------------------------------+
//| DailyTradeLimitEA.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
/*パラメーター*/
input int MaxTradesPerDay = 3;//1日に許可する新規エントリー回数の上限
input double Lots = 0.01;//ロット数
input int StopLossPips = 10;//損切り(pips)
input int TakeProfitPips = 5;//利確(pips)
input int MagicNumber = 20260910;//マジックナンバー
input int Slippage = 3;//スリッページ
//+------------------------------------------------------------------+
//| 当日0時(サーバー時刻)を求める |
//+------------------------------------------------------------------+
datetime GetTodayStart()
{
return(StrToTime(TimeToStr(TimeCurrent(), TIME_DATE)));
}
//+------------------------------------------------------------------+
//| 当日、このEA(MagicNumber)が新規エントリーした回数を数える |
//| 保有中(MODE_TRADES)と決済済み(MODE_HISTORY)の両方を合算する |
//+------------------------------------------------------------------+
int CountTodayEntries()
{
int count = 0;
datetime todayStart = GetTodayStart();
//保有中のポジション
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
continue;
if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
continue;
if(OrderType() != OP_BUY && OrderType() != OP_SELL)
continue;//指値・逆指値(未約定)は数えない
if(OrderOpenTime() >= todayStart)
count++;
}
//決済済みの履歴
for(int i = OrdersHistoryTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
continue;
if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
continue;
if(OrderType() != OP_BUY && OrderType() != OP_SELL)
continue;//入出金(OP_BALANCE/OP_CREDIT)は数えない
if(OrderOpenTime() >= todayStart)
count++;
}
return(count);
}
//+------------------------------------------------------------------+
//| 新しい足に切り替わった最初のティックかどうかを判定する |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(NULL, 0, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return(true);
}
return(false);
}
//+------------------------------------------------------------------+
//| エントリーを試みる(ここはダミー条件。実際のシグナルに差し替える) |
//+------------------------------------------------------------------+
void TryEntry()
{
int ticket = OrderSend(Symbol(), OP_BUY, Lots, Ask, Slippage,
Ask - StopLossPips * Point * 10,
Ask + TakeProfitPips * Point * 10,
"DailyTradeLimitEA", MagicNumber, 0, Blue);
if(ticket < 0)
Print("買い注文に失敗しました。エラーコード=", GetLastError());
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Comment("");
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
int todayCount = CountTodayEntries();
if(todayCount >= MaxTradesPerDay)
{
Comment("本日のエントリー回数: ", todayCount, " / ", MaxTradesPerDay,
"\n上限に達したため、これ以上エントリーしません。");
return;//ガードが働き、この先のシグナル判定・OrderSend自体を実行しない
}
Comment("本日のエントリー回数: ", todayCount, " / ", MaxTradesPerDay);
if(!IsNewBar())
return;
TryEntry();
}
//+------------------------------------------------------------------+
MetaEditorで実際に動かす手順
- MT4の「ツール」→「MetaQuotes Language Editor」でMetaEditorを開く
- 「ファイル」→「新規作成」を選び、ウィザードで「Expert Advisor(テンプレートを使用)」を選択して「次へ」を押す
- 名前を「DailyTradeLimitEA」と入力する(作者名・リンク・バージョンは空欄のままで問題ありません)。あとの画面はイベントハンドラのチェックを何も入れず、そのまま「次へ」を数回押して「完了」まで進める
- OnInit()・OnDeinit()・OnTick()の3つの関数だけが書かれた空のひな形が開くので、中身を全て削除し、上のコードを丸ごと貼り付ける
- F7キー(またはコンパイルボタン)でコンパイルし、「0 error(s), 0 warning(s)」になることを確認する
- MT4の「ナビゲーター」パネルからEAを探し、チャートへドラッグ&ドロップして「自動売買を許可する」にチェックを入れる
- チャート左上に「本日のエントリー回数: 0 / 3」のような表示が出て、新しい足が来るたびに回数が増え、上限に達すると「上限に達したため、これ以上エントリーしません。」に切り替われば成功です
コピペしてコンパイルが通らないときに確認すること
- 拡張子が「.mq4」になっているか
- `GetTodayStart()`・`CountTodayEntries()`・`IsNewBar()`・`TryEntry()`の4つの関数を、`OnInit()`より上(コードの先頭側)に配置しているか(MQL4は呼び出す関数を先に定義しておく必要があります)
- MaxTradesPerDayに0以下の値を設定していないか(0にすると初回のCountTodayEntries()が0を返した時点ですでに`todayCount >= MaxTradesPerDay`が成立し、一度もエントリーできません)
よくあるエラー・注意点
- 部分決済をするとカウントが二重になることがある: `OrderClose()`でポジションの一部だけを決済する運用をしている場合、決済した分だけ新しいチケット番号で履歴に記録され、元のチケットの`OrderOpenTime()`と同じ時刻が付きます。このEAのロジックは「開いた時刻が今日かどうか」だけで数えるため、1回のエントリーを部分決済すると2件として数えてしまう可能性があります。部分決済を使う運用では、チケット番号を記録して重複を除外する処理を追加してください
- ブローカーの日付境界が日本時間の0時と一致しない: 「今日」の判定は`TimeCurrent()`、つまりブローカーのサーバー時刻基準です。多くのFXブローカーはサーバー時刻をGMT+2や+3等に設定しているため、日本時間の午前6〜9時ごろに日付が切り替わります。「日本時間の0時にリセットされる」と誤解しないでください
- バックテストと実運用でカウントの検証精度が変わる: ストラテジーテスターでは「全ティック(最も正確な値)」モデルを選ばないと、ヒストリーデータの生成方法によって`OrderOpenTime()`の精度が実運用と異なる場合があります。このガードの動作を検証する際は、必ず「全ティック」モデルでバックテストしてください
- MagicNumberを他のEAと重複させない: 同じ口座で複数のEAを動かしている場合、MagicNumberが重複していると、他のEAが送った注文までこのEAの「今日のエントリー回数」に混入します。EAごとに必ず異なるMagicNumberを設定してください
まとめ
オーバートレード対策というと損益額や連敗数を使った複雑なロジックを想像しがちですが、
「当日の新規エントリー回数が上限に達したら、その日はそれ以上エントリーしない」
という回数ベースの歯止めだけでも、感情的な連続エントリーには十分な抑止力になります。
今回のCountTodayEntries()は、カウンター変数を持たずに毎回注文履歴から数え直す設計にしているため、
EAやターミナルの再起動をまたいでも正しい実績を保ち続けます。
既存のエントリーEAのOnTick()先頭に、このガードの判定部分をそのまま追加するだけで組み込めます。
コメント