取引規律をコードに組み込む(第2回):MQL5で口座全体の全取引に対応する日次取引制限エンフォーサーを構築する
内容
はじめに
1日の取引回数を制限することは、トレーダーが設定できる最も基本的なリスク管理ルールの一つです。しかし実際の運用環境では、このルールは最も簡単に破られてしまうルールの一つでもあります。これは裁量トレーダーだけに当てはまる問題ではありません。複数のエキスパートアドバイザー(EA)が複数の銘柄で同時に動作している口座においても同様です。たとえば、「1日に許可されるエントリーは最大N回まで」というルールを設定したとしても、市場からのプレッシャーが高まった時、連敗や連勝によって心理状態が変化した時、あるいは複数の戦略が個別に動作している場合には、そのルールは簡単に機能しなくなります。
本質的な問題は、トレーダーに規律が存在しないことではありません。問題は、口座レベルで規律を強制する仕組みが存在しないことです。MT5には、何をエントリーとして扱うのか、取引日をどのように測定するのか、そして事前に定義した制限に到達した場合に何を実行するのかを定義する、一元的な仕組みは標準では提供されていません。エントリーをDEAL_ENTRY_INに基づいて判定するのか、ポジションを基準にするのか、注文を基準にするのか。また、セッションをサーバー時間の0時でリセットするのか、カスタム時間でリセットするのか。さらに、制御をすべての銘柄に適用するのか。これらを明確に定義しなければ、ルールは概念的なものに留まり、実際の制御構造にはなりません。
本記事では、口座レベルで動作するMQL5製の日次取引制限エンフォーサーを実装します。このシステムは、設定可能なセッション時間内で確定したエントリーディールをカウントし、すべての銘柄にわたる取引活動を監視し、制限を自動的に強制します。定義された上限に到達すると、新しい非成行注文は削除され、新しく建てられたポジションは決済されます。一方で、制限到達前に保有していた既存のポジションには影響を与えません。
本記事では、行動に関するリスク管理ルールを、技術的に強制可能な制約へ変換する方法に焦点を当てます。アーキテクチャ、実装の詳細、テスト手順を、モジュール化され再現可能な形で説明します。記事の最後では、共有ロジック層、ダッシュボードコンポーネント、制限を強制するEAを含む完全なソースコードを提供します。
規律ロジックの理解
規律については、取引に関する文献で広く議論されています。しかし、実際の運用においてそれを維持することは、最も難しい資質の一つです。ルールを定義し、文書化し、さらに他者にそれを守るよう促すことは簡単です。しかし、実際の市場環境において同じルールを一貫して適用することは、まったく異なる課題になります。取引規律が低下する原因は、トレーダーがルールを忘れるからではありません。感情的および認知的なプレッシャーが時間の経過とともに蓄積するためです。一つひとつの判断が、認識、信頼感、リスク許容度を少しずつ変化させます。その結果、多くの場合、本人がすぐに気付かないまま、行動は徐々に当初の計画から離れていきます。
取引規律に影響を与える要因
取引セッション中の規律の低下には、いくつかの要因が直接影響します。これらには、以下のものが含まれますが、これらに限定されません。
- 連続した勝敗によって引き起こされる感情の増幅
- 特にボラティリティの高い市場局面における時間的プレッシャー
- 最近の結果が意思決定を支配する結果への固執
- 同じセッション内で損失を取り戻そうとするリカバリーバイアス
- 短期的な成功によって引き起こされる根拠の薄い自信
これらの要因は、それぞれ異なる形で行動に影響を与えます。しかし、共通する結果があります。それは、最も重要な局面でルールの遵守が弱まることです。重要なのは、こうしたプレッシャーは経験の浅い参加者だけに起こるものではないという点です。経験豊富なプロフェッショナルであっても、特に長時間のセッション中や通常とは異なる市場環境において、規律が崩れる期間を経験します。経験によってこのような失敗の頻度を減らすことはできますが、脆弱性を完全になくすことはできません。
このような心理的プレッシャーは、意識的な認識を上回る可能性があります。そのため、警告だけに基づいて構築されたシステムは、本当に必要とされる場面で機能しない可能性があります。これが、ロジックを受動的な監視だけではなく、能動的な制御へ拡張する理由です。あらかじめ定義された制限に到達すると、システムは自動的に介入し、衝動的に行動することを防ぎ、構造的な規律を維持します。
取引規律の自動化
これらの現実を考慮すると、自己管理だけに完全に依存することは不十分になります。人間の心理は、不確実性、迅速なフィードバック、そして金銭的リスクに長時間さらされる環境に対応するようには設計されていません。これこそが、自動化が一貫性を維持する上で重要な役割を果たす理由です。
規律を自動化することは、制御をアルゴリズムに明け渡すことを意味しません。そうではなく、監視、カウント、通知の責任を、感情の影響を受けないシステムへ移すことを意味します。これらの機能を外部化することで、トレーダーは認知的負荷と意思決定疲労を軽減できます。このアプローチは、規律を個人的な苦闘ではなく、構造的な安全対策として捉え直します。
本システムにおける規律ロジック
日次取引制限システムは、2層構造のロジックを中心に構築されています。リアルタイムフィードバックによる認識と、自動介入による制限の強制です。その設計は複雑さを避け、状態遷移を明確に保ちながら、制限を超えた場合に重要な安全機能を追加しています。
システムは動作中に以下の機能を実行します。
1. 定義された取引日において、口座全体で実行された取引数を追跡します(すべての銘柄、手動取引またはEAによる取引を対象)。2. そのカウントをユーザーが定義した最大値と比較します。
3. 制限までの近さに基づいて、現在の取引状態を分類します。
- 許可(Allowed):取引数が制限内にある状態です。
- 注意(Caution):残りの取引可能回数が警告閾値以下になった状態です。
- 制限到達(Limit Reached):最大取引回数が実行された状態です。
4. 色分けされたダッシュボードと、任意のアラート(ポップアップ、プッシュ通知、音声)を通じて、現在の状態をトレーダーに通知します。
5. 「制限到達」状態に入ると、新しい取引を積極的にブロックすることで制限を強制します。
- 制限到達後に配置された新しい未決注文は、直ちに削除されます。
- 新しくオープンされたポジション(EAによるもの、または手動によるもの)は、直ちに決済されます。
- 制限到達前にオープンされた既存のポジションは変更されません。システムは追加のエクスポージャーのみを防止します。
この2層構造により、トレーダーは常に自身の状態を把握できます。しかし、プレッシャーによって認識が弱まった場合でも、構造的な強制によってさらなる過剰取引を防止します。このロジックは、以下のように視覚化できます。

図1:1日の開始から制限の強制までの判断フロー
このフローは、システムが継続的に実行された取引を追跡し、それらを事前に定義された1日の制限と比較し、現在の取引状態を分類する方法を示しています。処理は取引日の開始時点から始まり、口座全体のすべての取引が監視されます。各更新後、システムは取引が許可された範囲内に維持されているかを評価します。
状態が「許可」の場合、取引は制限なく継続されます。システムが制限に近づいているものの、まだ到達していないことを検出すると、「注意」状態へ移行します。この状態では警告が発行されますが、取引活動は引き続き許可されます。これは、即時に制限することなく、早期の認識を促します。
1日の制限に到達すると、システムは「制限到達」状態へ移行します。この段階では、適用は単なる情報提供ではなく、実際の介入になります。新しい非成行注文はキャンセルされ、新しく建てられたポジションは直ちに決済されます。一方で、それ以前から存在していたポジションには影響を与えません。
この「許可 → 注意喚起 → 制限の強制」という構造化された進行は、監視が分類につながり、分類がアクションを決定するという、実際の制御システムと同じ考え方に基づいています。目的は単に通知することではありません。あらかじめ定義された境界を超えた時点で、決断力を持って介入することです。
システム設計概要
コードの詳細を確認する前に、日次取引制限エンフォーサーの全体的なアーキテクチャを理解することが重要です。単一のモノリシックなエキスパートアドバイザー(EA)を構築するのではなく、このシステムは意図的に、独立しながらも連携する3つのコンポーネントに分割されています。
このモジュール化されたアプローチにより、各コンポーネントには明確な責任が与えられ、他のコンポーネントに影響を与えることなく、変更、置換、または拡張することができます。コンポーネント間の通信には、ターミナルのグローバル変数を使用します。これは、任意のMQL5プログラムからアクセス可能な共有メモリ層として機能します。
3つのコンポーネントは以下の通りです。
1. 共有ライブラリ:DailyTradeLimit.mqh
このインクルードファイルには、システムの中核ロジックが含まれています。担当する処理は以下の通りです。
- 現在の取引日に実行された取引のカウント
- 設定された開始時間に基づいた現在の取引日の判定
- 残りの取引可能回数の計算
- 取引状態の分類(許可 / 注意 / 制限到達)
このライブラリは、IsTradingAllowed()、TradesToday()、GetState()などの軽量な関数を提供します。これらは、任意のEAまたはインジケーターから呼び出すことができます。
ロジックを共有ライブラリ内に配置することで、すべてのコンポーネントが同じ計算ルールと一貫したデータを使用できるようになります。これにより、処理の重複を排除し、モジュール間でロジックが異なる状態になることを防ぎます。
2. ダッシュボードインジケータ:DailyTradeLimitDashboard.mq5
ダッシュボードは、ユーザーインターフェース層として機能します。チャートに適用すると、以下の処理をおこないます。
- ユーザーが定義した入力値(1日の制限、開始時間、アンバー閾値、アラート設定)を読み取る
- これらの設定をグローバル変数へ書き込む
- 以下を表示するカラーコード付きパネルを表示する
- 本日実行された取引数
- 残りの取引可能回数
- 現在の状態
ダッシュボードは1秒ごと、および取引イベント発生直後に更新され、リアルタイムで状態を確認できるようにします。また、取引状態が変更された場合には、ポップアップ、プッシュ通知、またはサウンドによるアラートを生成できます。その役割は、認識と透明性の提供です。
3. エンフォーサーEA:DailyTradeLimitEnforcer.mq5
エンフォーサーEAは、制御レイヤーとしてバックグラウンドで継続的に動作します。このEAは、ダッシュボードによって定義された同じグローバル変数を読み取り、口座全体のすべての取引活動を監視します。新しい取引が検出されるたびに、1日の取引制限にすでに到達しているかを確認します。
制限を超えている場合、EAは直ちに以下を実行します。
- 新しい非成行注文をキャンセルする
- 新規建てされたポジションを決済する
これは、その取引が手動で実行されたものか、別のEAによって開始されたものかに関係なく適用されます。重要なのは、制限到達前に建てられたポジションには影響を与えないことです。このシステムは追加のエクスポージャーを防止するものであり、過去の取引に遡って介入するものではありません。
グローバル変数による通信
3つのコンポーネントは、以下の共有グローバル変数を使用します。
- DTL_LIMIT:1日に許可される最大取引数(double型として保存されますが、整数として扱われます)
- DTL_START_TIME:HHMM形式でエンコードされた取引日の開始時間(例:09:00の場合は900)
- DTL_AMBER:注意状態の閾値(残りの取引可能回数がこの値以下の場合)
ダッシュボードは初期化時にこれらの値を書き込みます。ライブラリとエンフォーサーは、再計算が必要になるたびにこれらの値を読み取ります。取引数のカウントロジックは共有ライブラリ内に配置され、定期的な更新処理を使用するため、すべてのコンポーネントは同期された同一のデータ状態で動作します。
以下の図は、ダッシュボード、共有ライブラリ、エンフォーサーEAの間の相互作用の流れを示しています。

図2:相互作用フロー
なぜこの設計なのか?
システムを3つの連携するモジュールに分割することで、いくつかの構造的な利点が得られます。
| 利点 | 説明 |
|---|---|
| 再利用性 | 任意のEAが共有ライブラリをインクルードし、IsTradingAllowed()を使用して自主的に確認できます。仮に確認処理をおこなわない場合でも、エンフォーサーEAが制限遵守を保証します。 |
| 独立性 | ダッシュボードとエンフォーサーは、それぞれ別のチャート上で動作できます。どちらも他のEAや手動取引のワークフローに干渉しません。 |
| 明確性 | 各コンポーネントは単一責任の原則に従っており、システムを理解、テスト、保守しやすくします。 |
| 拡張性 | 1日の利益上限、ドローダウン制限、時間ベースの取引時間制御などの追加制約は、同じアーキテクチャパターンを維持したまま共有ライブラリを拡張することで実装できます。 |
このアーキテクチャの基盤が確立されたので、次に各コンポーネントの段階的な実装へ進みます。
MQL5での実装
このセクションでは、規律フレームワークを完全に機能するMQL5コードへ変換する実際の手順を、段階的に進めていきます。目的は、単に取引数をカウントすることではありません。実際の運用環境において、安定性があり、読みやすく、適応可能なシステムを設計することです。
開発はMetaEditor環境から開始します。ここでは、3つの個別ファイルを作成し、それぞれに明確な役割を割り当てます。最初から、設定入力、状態管理、計算ロジック、ユーザーインターフェース要素を厳密に分離します。この構造的な明確性により、システムはモジュール化され、テスト可能であり、後から追加の制約を導入する場合でも容易に拡張できる状態を維持できます。
A) DailyTradeLimit.mqh:共有ライブラリ
- 時間変換ユーティリティ
このライブラリでは、TimeToIntとIntToTimeを提供し、人間が読みやすい時間入力とターミナルの保存形式を橋渡しします。TimeToIntは「09:00」のような文字列を整数900に変換します。この形式は、ターミナルのグローバル変数へ保存する場合に便利です。IntToTimeは逆の処理をおこない、900を表示や処理用の「09:00」へ戻します。このエンコード方式により、設定情報を軽量に保ちながら、容易に解釈できる形式を維持できます。
//+------------------------------------------------------------------+ //| Convert "HH:MM" to HHMM (e.g., "09:00" -> 900) | //+------------------------------------------------------------------+ int TimeToInt(string timeStr) { string parts[]; if(StringSplit(timeStr,':',parts)!=2) return 0; int h=(int)StringToInteger(parts[0]); int m=(int)StringToInteger(parts[1]); return h*100+m; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Convert HHMM to "HH:MM" | //+------------------------------------------------------------------+ string IntToTime(int value) { int h=value/100; int m=value%100; return StringFormat("%02d:%02d",h,m); } //+------------------------------------------------------------------+
- パラメータの取得
設定情報は、GetParamLimit、GetParamStartTime、GetParamAmberを通じてターミナルのグローバル変数から読み取られます。各関数は対応するグローバル変数(DTL_LIMIT、DTL_START_TIME、DTL_AMBER)を読み取り、グローバル変数が設定されていない場合は適切なデフォルト値を使用します。これにより、ダッシュボードとエンフォーサーを含むすべてのコンポーネントが、ロジックを重複させることなく、同じ設定元を共有できます。
//+------------------------------------------------------------------+ //| Get the daily trade limit (max trades per day) | //+------------------------------------------------------------------+ int GetParamLimit() { if(GlobalVariableCheck("DTL_LIMIT")) return (int)GlobalVariableGet("DTL_LIMIT"); return DEFAULT_LIMIT; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Get the configured start time as a string | //+------------------------------------------------------------------+ string GetParamStartTime() { if(GlobalVariableCheck("DTL_START_TIME")) { int t=(int)GlobalVariableGet("DTL_START_TIME"); return IntToTime(t); } return DEFAULT_START_TIME; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Get the amber threshold (remaining trades before caution) | //+------------------------------------------------------------------+ int GetParamAmber() { if(GlobalVariableCheck("DTL_AMBER")) return (int)GlobalVariableGet("DTL_AMBER"); return DEFAULT_AMBER; } //+------------------------------------------------------------------+
- 時間計算
ParseTimeは、「HH:MM」形式の文字列を個別の時と分の整数値へ変換します。これを使用してGetDayStartは現在の取引日の基準となる開始時刻を設定します。GetDayStartは、ブローカー時間と設定された開始時間を比較します。現在時刻が当日の開始時間より前の場合は、前日を基準時刻として使用します。それ以外の場合は、当日を基準時刻として使用します。この正確な計算により、異なるブローカーのタイムゾーン環境においても、1日の取引数が正しいタイミングでリセットされることが保証されます。
//+------------------------------------------------------------------+ //| Parse "HH:MM" into hour and minute integers | //+------------------------------------------------------------------+ bool ParseTime(string txt,int &h,int &m) { string parts[]; if(StringSplit(txt,':',parts)<2) return false; h=(int)StringToInteger(parts[0]); m=(int)StringToInteger(parts[1]); return (h>=0 && h<=23 && m>=0 && m<=59); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Calculate the start datetime of the current trading day | //+------------------------------------------------------------------+ datetime GetDayStart(datetime now,string startTime) { int h=0,m=0; if(!ParseTime(startTime,h,m)) { h=0; m=0; } MqlDateTime t; TimeToStruct(now,t); //--- If current time is before today's start, use yesterday if(t.hour<h || (t.hour==h && t.min<m)) now-=86400; TimeToStruct(now,t); t.hour=h; t.min=m; t.sec=0; return StructToTime(t); } //+------------------------------------------------------------------+
- 取引数のカウント
CountTradesは、計算された取引日の開始時刻以降に発生した、DEAL_ENTRY_INを持つすべての約定をカウントします。これは、当日の時間範囲内で履歴を検索するためにHistorySelectを使用し、その範囲内の約定を順番に確認して、DEAL_ENTRY_INを持つものを数えます。
//+------------------------------------------------------------------+ //| Count trades (DEAL_ENTRY_IN) since a given datetime | //+------------------------------------------------------------------+ int CountTrades(datetime from) { datetime to=from+86400; if(!HistorySelect(from,to)) return 0; int total=HistoryDealsTotal(); int cnt=0; for(int i=0; i<total; i++) { ulong ticket=HistoryDealGetTicket(i); if(ticket>0 && HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_IN) cnt++; } return cnt; } //+------------------------------------------------------------------+
- 状態計算
CalcStateは、取引数を1日の取引制限およびアンバー閾値と比較し、STATE_ALLOWED、STATE_CAUTION、またはSTATE_LIMITを返します。これにより、数値によるカウントから明確な取引状態への変換を、シンプルかつ一貫した形で実現します。
//+------------------------------------------------------------------+ //| Map the trade count to a state (allowed/caution/limit) | //+------------------------------------------------------------------+ ENUM_DTL_STATE CalcState(int trades,int limit,int amber) { if(trades>=limit) return STATE_LIMIT; if(limit-trades<=amber) return STATE_CAUTION; return STATE_ALLOWED; } //+------------------------------------------------------------------+
- キャッシュ管理
このライブラリは、最後の更新時刻、取引日の開始時刻、本日の取引数、現在の状態を保持するための内部キャッシュ変数を管理します。Refreshは、履歴スキャンが頻繁に実行されることを防ぐため、最小更新間隔を1秒に設定しています。キャッシュが古くなっている場合、RefreshはForceRefreshを呼び出します。ForceRefreshは完全な再カウントを実行し、すべてのキャッシュ値を更新します。ForceRefreshは、取引日の開始時刻、本日の取引数、状態を再計算し、更新時刻を保存します。このキャッシュ処理により、応答性とパフォーマンスのバランスを維持しています。
//+------------------------------------------------------------------+ //| Cached refresh – only performs a full update every 1 second | //+------------------------------------------------------------------+ bool Refresh() { datetime now=TimeCurrent(); if(now-s_lastRefresh<REFRESH_INTERVAL) return false; return ForceRefresh(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Force a full recount and update all cache values | //+------------------------------------------------------------------+ bool ForceRefresh() { datetime now=TimeCurrent(); int limit=GetParamLimit(); string st=GetParamStartTime(); int amber=GetParamAmber(); datetime newDayStart=GetDayStart(now,st); int newTrades=0; if(newDayStart!=s_dayStart) { s_dayStart=newDayStart; newTrades=CountTrades(s_dayStart); } else { newTrades=CountTrades(s_dayStart); } ENUM_DTL_STATE newState=CalcState(newTrades,limit,amber); bool changed=(newState!=s_state) || (newTrades!=s_tradesToday); s_tradesToday=newTrades; s_state=newState; s_lastRefresh=now; return changed; } //+------------------------------------------------------------------+
- グローバルパラメータ設定
SetParametersは、ユーザーの入力値をターミナルのグローバル変数(DTL_LIMIT、DTL_START_TIME、DTL_AMBER)へ保存します。これにより、すべてのコンポーネントが同じ情報源から値を読み取り、一貫して動作することが保証されます。また、次回アクセス時に新しい計算を強制的に実行するため、キャッシュをリセットします。
//+------------------------------------------------------------------+ //| Store configuration into terminal globals | //+------------------------------------------------------------------+ bool SetParameters(int limit,string startTime,int amber) { if(!GlobalVariableSet("DTL_LIMIT",limit)) return false; int t=TimeToInt(startTime); if(!GlobalVariableSet("DTL_START_TIME",t)) return false; if(!GlobalVariableSet("DTL_AMBER",amber)) return false; s_lastRefresh=0; // force refresh next time return true; } //+------------------------------------------------------------------+
B) DailyTradeLimitDashboard.mq5:ビジュアルダッシュボード
- 初期化
ダッシュボードは、共有ライブラリをインクルードすることから開始し、ライブラリのデフォルト値に対応する入力項目を公開します。初期化時には、DTL::SetParametersを呼び出してユーザーの入力値をターミナルのグローバル変数へ反映します。その後、表示用パネルを作成し、キャッシュへ初期値を反映するために初回更新を実行します。さらに、継続的に更新するため、1秒間隔のタイマーを開始します。
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Propagate user inputs into globals DTL::SetParameters(InpDailyTradeLimit,InpDayStartTime,InpAmberRemainingTrades); //--- Build UI and prime data CreatePanel(); DTL::Refresh(); prevState=DTL::GetState(); //--- Start timer for updates EventSetTimer(1); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
- CreatePanel()
CreatePanelは、チャート上にコンパクトで自己完結型のパネルを作成します。最初に、パネルのプレフィックスを共有する既存のオブジェクトをすべて削除し、不要なオブジェクトが残らないようにします。その後、背景用の矩形と4つのラベル(TITLE、L1、L2、L3)を描画します。パネルのレイアウトは、明確で読みやすい表示になるように設計されており、色は取引状態を反映するように選択されています。
//+------------------------------------------------------------------+ //| Create the entire dashboard panel | //+------------------------------------------------------------------+ void CreatePanel() { DeleteByPrefix(PREFIX); ObjectCreate(0,PREFIX+"BG",OBJ_RECTANGLE_LABEL,0,0,0); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_CORNER,InpCorner); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_XDISTANCE,InpX); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_YDISTANCE,InpY); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_XSIZE,InpWidth); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_YSIZE,InpHeight); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_BACK,false); ObjectSetInteger(0,PREFIX+"BG",OBJPROP_SELECTABLE,false); string labels[4]={"TITLE","L1","L2","L3"}; int y[4]={8,32,52,78}; for(int i=0; i<4; i++) { ObjectCreate(0,PREFIX+labels[i],OBJ_LABEL,0,0,0); ObjectSetInteger(0,PREFIX+labels[i],OBJPROP_CORNER,InpCorner); ObjectSetInteger(0,PREFIX+labels[i],OBJPROP_XDISTANCE,InpX+10); ObjectSetInteger(0,PREFIX+labels[i],OBJPROP_YDISTANCE,InpY+y[i]); ObjectSetInteger(0,PREFIX+labels[i],OBJPROP_COLOR,clrWhite); ObjectSetInteger(0,PREFIX+labels[i],OBJPROP_SELECTABLE,false); } ObjectSetInteger(0,PREFIX+"TITLE",OBJPROP_FONTSIZE,11); ObjectSetString(0,PREFIX+"TITLE",OBJPROP_TEXT,"DAILY TRADE LIMIT"); ObjectSetInteger(0,PREFIX+"L1",OBJPROP_FONTSIZE,10); ObjectSetInteger(0,PREFIX+"L2",OBJPROP_FONTSIZE,10); ObjectSetInteger(0,PREFIX+"L3",OBJPROP_FONTSIZE,10); } //+------------------------------------------------------------------+
- UpdatePanel()
UpdatePanelは、TradesToday、GetParamLimit、Remaining、GetStateを使用して、ライブラリからリアルタイムデータを読み取り、パネル内のテキスト表示を更新します。また、現在の状態を視覚的に確認できるように、状態に応じて背景色を変更します。STATE_ALLOWEDの場合は緑色、STATE_CAUTIONの場合はオレンジ色、 STATE_LIMITの場合はマルーン色に設定され、現在の状態を即座に視覚的に把握できるようになります。
//+------------------------------------------------------------------+ //| Refresh the panel's content with the latest state | //+------------------------------------------------------------------+ void UpdatePanel() { int trades=DTL::TradesToday(); int limit=DTL::GetParamLimit(); int remaining=DTL::Remaining(); DTL::ENUM_DTL_STATE state=DTL::GetState(); ObjectSetString(0,PREFIX+"L1",OBJPROP_TEXT, StringFormat("Trades today: %d / %d",trades,limit)); ObjectSetString(0,PREFIX+"L2",OBJPROP_TEXT, StringFormat("Remaining today: %d",remaining)); ObjectSetString(0,PREFIX+"L3",OBJPROP_TEXT, StringFormat("STATUS: %s",DTL::StateToString(state))); color bg=clrNONE; switch(state) { case DTL::STATE_ALLOWED: bg=clrDarkGreen; break; case DTL::STATE_CAUTION: bg=clrOrange; break; case DTL::STATE_LIMIT: bg=clrMaroon; break; } ObjectSetInteger(0,PREFIX+"BG",OBJPROP_BGCOLOR,bg); } //+------------------------------------------------------------------+
- タイマー処理(OnTimer)
OnTimerはダッシュボードの心臓部として機能します。内部状態を更新するためにRefresh関数を呼び出し、次に表示を更新するためにUpdatePanel関数を呼び出します。状態が変化し、アラートが有効になっている場合、端末アラート、プッシュ通知(有効になっている場合)、およびオプションで音を鳴らして変化を知らせます。
//+------------------------------------------------------------------+ //| Timer event – updates the panel and triggers alerts | //+------------------------------------------------------------------+ void OnTimer() { bool changed=DTL::Refresh(); UpdatePanel(); if(changed && InpShowAlerts) { string msg; switch(DTL::GetState()) { case DTL::STATE_ALLOWED: msg="Trading allowed: within daily trade limit."; break; case DTL::STATE_CAUTION: msg="Caution: approaching daily trade limit."; break; case DTL::STATE_LIMIT: msg="Trading limit reached: no trades remaining today."; break; } Alert(msg); if(InpUsePushNotifications) SendNotification(msg); if(InpPlaySound) PlaySound(InpSoundFile); } } //+------------------------------------------------------------------+
C) DailyTradeLimitEnforcer.mq5:エンフォーサーEA
- 初期化
エンフォーサーは共有ライブラリをインクルードし、ForceRefreshを使用して最新の状態を取得します。内部で使用する小さなフラグ(s_limitJustReached)によって、制限状態への移行を検出し、制限が有効になった正確なタイミングでクリーンアップ処理を実行できるようにします。また、1秒間隔のタイマーを開始し、状態を定期的に再確認するとともに、制限に到達した直後にクリーンアップ処理を実行します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { Print("DailyTradeLimitEnforcer started. AutoTrading must be enabled."); EventSetTimer(1); DTL::ForceRefresh(); // initial read s_limitJustReached=(DTL::GetState()==DTL::STATE_LIMIT); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); Print("DailyTradeLimitEnforcer stopped."); } //+------------------------------------------------------------------+
- タイマールーチン(OnTimer)
OnTimerは、最新の情報を取得するためにForceRefreshを呼び出します。現在の状態がSTATE_LIMITであり、かつ制限に到達した直後である場合(フラグによって状態遷移を検出)、CancelAllPendingOrdersを通じてすべての保留中の注文をキャンセルします。これにより、制限が有効になる前に配置された注文から新しい取引が発生することを防ぎます。その後、フラグは現在の状態遷移状況を反映するように更新されます。
//+------------------------------------------------------------------+ //| Timer – checks for limit state and cancels pending orders | //+------------------------------------------------------------------+ void OnTimer() { DTL::ForceRefresh(); bool nowLimited=(DTL::GetState()==DTL::STATE_LIMIT); //--- If we just entered the limit, cancel all pending orders if(nowLimited && !s_limitJustReached) { Print("Daily limit reached – cancelling all pending orders."); CancelAllPendingOrders(); } s_limitJustReached=nowLimited; } //+------------------------------------------------------------------+
- 取引トランザクションへの対応(OnTradeTransaction)
OnTradeTransactionは、新しい取引活動に対して即時に対応します。ForceRefreshを実行した後、まだ取引が許可されている場合、この関数は何も処理をおこなわずに終了します。制限に到達している場合は、以下の2つのケースを処理します。
- 新しく追加された非成行注文の場合:エンフォーサーは、それが取引になることを防ぐため、直ちにキャンセルします。
- 新たに発生した新規約定の場合:関連するポジションを特定し、反対方向の成行注文によって決済します。
このアプローチにより、制限到達前にオープンされたポジションに影響を与えることなく、追加のエクスポージャーを防止します。
//+------------------------------------------------------------------+ //| Trade transaction handler – blocks any disallowed trade | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { //--- Ensure we have latest state DTL::ForceRefresh(); //--- If still allowed, nothing to do if(DTL::IsTradingAllowed()) return; //--- Case: new pending order added if(trans.type==TRADE_TRANSACTION_ORDER_ADD) { ulong orderTicket=trans.order; if(orderTicket==0) return; if(!OrderSelect(orderTicket)) { Print("Failed to select order ",orderTicket); return; } ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL) return; // market order – will be handled if filled MqlTradeRequest req={}; MqlTradeResult res={}; req.action =TRADE_ACTION_REMOVE; req.order =orderTicket; req.comment ="Daily limit reached – order cancelled immediately"; if(OrderSend(req,res)) Print("Immediately cancelled pending order ",orderTicket); else Print("Immediate cancel failed for order ",orderTicket,": ",res.comment); } //--- Case: new deal added (opening trade) if(trans.type==TRADE_TRANSACTION_DEAL_ADD) { ulong dealTicket=trans.deal; if(dealTicket==0) return; if(HistoryDealGetInteger(dealTicket,DEAL_ENTRY)!=DEAL_ENTRY_IN) return; ulong posTicket=HistoryDealGetInteger(dealTicket,DEAL_POSITION_ID); if(posTicket==0) return; if(!PositionSelectByTicket(posTicket)) { Print("Position ",posTicket," no longer open."); return; } //--- Close newly opened position MqlTradeRequest req={}; MqlTradeResult res={}; req.action =TRADE_ACTION_DEAL; req.position =posTicket; req.symbol =PositionGetString(POSITION_SYMBOL); req.volume =PositionGetDouble(POSITION_VOLUME); req.deviation =5; req.comment ="Daily limit reached – new position closed immediately"; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { req.price=SymbolInfoDouble(req.symbol,SYMBOL_BID); req.type=ORDER_TYPE_SELL; } else { req.price=SymbolInfoDouble(req.symbol,SYMBOL_ASK); req.type=ORDER_TYPE_BUY; } if(OrderSend(req,res)) Print("Immediately closed new position ",posTicket); else Print("Immediate close failed for position ",posTicket,": ",res.comment); } } //+------------------------------------------------------------------+
- ヘルパー関数(CancelAllPendingOrders)
CancelAllPendingOrdersは、すべての注文を反復処理し、非成行注文を削除します。成行注文は対象外となります。これは、成行注文が事前に配置された注文ではなく、通常の価格変動によって処理されるものだからです。
//+------------------------------------------------------------------+ //| Cancel all pending orders (market orders are ignored) | //+------------------------------------------------------------------+ void CancelAllPendingOrders() { for(int i=OrdersTotal()-1; i>=0; i--) { ulong ticket=OrderGetTicket(i); if(ticket==0) continue; if(!OrderSelect(ticket)) { Print("Failed to select order ",ticket); continue; } ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL) continue; // market order – ignore MqlTradeRequest req={}; MqlTradeResult res={}; req.action =TRADE_ACTION_REMOVE; req.order =ticket; req.comment ="Daily limit reached – order cancelled"; if(OrderSend(req,res)) Print("Cancelled pending order ",ticket); else Print("Failed to cancel order ",ticket,": ",res.comment); } } //+------------------------------------------------------------------+
3つのコンポーネント、すなわち、規律ロジックの中核を含む共有ライブラリ、リアルタイムで状況を把握するためのビジュアルダッシュボード、そして制限到達後の追加取引を積極的にブロックする独立したエンフォーサーEAがすべて実装されたことで、システムは完成しました。ターミナルのグローバル変数を介した通信により、すべてのコンポーネントが同じ設定と最新の取引数を使用して動作することが保証されます。
モジュール化された設計により柔軟性も提供されます。ダッシュボードとエンフォーサーを組み合わせて完全な保護機能として使用できるほか、ライブラリをカスタムEAへ統合して自主的に制限を確認することもできます。その場合でも、エンフォーサーが信頼性の高い安全策として機能します。
MetaEditorでファイルを作成する
自身のプラットフォームにこのシステムを実装するには、以下の手順に従ってMetaEditorで必要な3つのファイルを作成します。
共有ライブラリ(.mqh)の作成
- MetaEditorで[新規作成]→[インクルードファイル(.mqh)]を選択します。
- ファイル名をDailyTradeLimit.mqhとし、MQL5\Include\フォルダに保存します。
- 共有ライブラリの完全なコードをこのファイルへコピーして貼り付け、保存します。(インクルードファイルは他のプログラムから参照されるものであり、個別にコンパイルする必要はありません。)
ダッシュボードインジケータ(.mq5)の作成
- [新規作成]をクリックし、[カスタムインディケータ]を選択します。
- ファイル名をDailyTradeLimitDashboard.mq5とし、MQL5\Indicators\フォルダに保存します。
- ウィザードではデフォルト設定をそのまま使用します。(インジケータバッファを手動で追加する必要はありません。コード内ですでに処理されています。)
- 生成されたコードを、提供されている完全なダッシュボードコードへ置き換え、その後コンパイルします(Ctrl+F7)。
エンフォーサーEA(.mq5)の作成
- [新規作成]をクリックし、[エキスパートアドバイザ]を選択します。
- ファイル名をDailyTradeLimitEnforcer.mq5とし、MQL5\Experts\フォルダに保存します。
- テンプレートコードを完全なエンフォーサーEAコードへ置き換え、その後コンパイルします。
すべてのファイルが正常に作成されたら、ダッシュボードを任意のチャートへ適用できます([ナビゲータ]の「Indicators」)。また、エンフォーサーEAも任意のチャートへ適用できます([ナビゲータ]の「Expert Advisors」)。エンフォーサーが機能するように、ターミナルでアルゴリズム取引が有効になっていることを確認してください。次のセクションでは、実際の市場環境下でシステムをテストし、その正確性、応答性、そして最も重要な点として、例外なく1日の取引制限を強制できる能力を検証します。
テスト
共有ライブラリ、ビジュアルダッシュボード、エンフォーサーEAで構成される完全なデイリー取引制限システムは、その正確性、応答性、そして最も重要な点として、定義された制限に到達した後に新しい取引を積極的に防止できる能力を検証するため、MetaTrader 5実際のチャート環境でテストされました。
テストでは、口座全体における正確な取引数のカウント、リアルタイムでの状態遷移とダッシュボード更新、制限超過後の取引活動に対するエンフォーサーの即時対応、取引日の切り替わり時における正しい動作の4つの主要な領域に重点を置きました。各コンポーネントは、内部的な正確性を確認するために個別に評価され、その後、完全に統合されたシステムの一部としてテストされました。これにより、現実的な条件下でも一貫性があり、信頼性の高い強制がおこなわれることを確認しました。
テスト環境の概要
システムは以下の条件下で評価されました。
| 側面 | 説明 |
|---|---|
| プラットフォーム | MetaTrader 5 |
| 口座タイプ | デモ口座 |
| 実行タイプ | 手動による取引実行 |
| 稼働中のコンポーネント | 1. 共有ライブラリ(DailyTradeLimit.mqh) 2. ダッシュボードインジケータ(DailyTradeLimitDashboard.mq5) |
| 目的 | 実際のチャート環境におけるカウント精度、状態遷移、強制動作の検証 |
このテストでは、意図的に手動でおこなった取引を使用しました。これにより、強制が自動システムだけでなく、すべての取引方法に対して有効であることを確認します。
フェーズ1:初期状態と段階的な状態遷移

図3:初期状態と段階的な状態遷移
最初の図は、取引セッション開始時の状態を示しています。
観察結果:
- 本日の取引数:0 / 5
- 残りの取引可能回数:5
- 状態:TRADING ALLOWED
- ダッシュボードの色:緑
これは、以下を確認するものです。
- 取引日が正しく初期化された
- 取引カウンタが0から開始された
- システムが状態を正しくSTATE_ALLOWEDと分類した
- グローバル変数が正しく読み取られ、同期された
手動で取引が実行されると、システムは以下の状態遷移を進みました。
許可(Allowed)
取引数が閾値を十分下回っている間、取引は制限なく継続されました。
注意(Caution)
残りの取引可能回数が設定されたアンバー閾値に近づくと、システムは注意状態へ移行しました。ダッシュボードの色はそれに応じて変更され、状態はSTATE_CAUTIONに更新されました。これにより、制限が適用される前に明確な視覚的な認識が提供されます。この動作により、システムが1日の制限への接近を正しく検出し、アンバー閾値を正確に評価し、パネルをリアルタイムで更新し、各取引イベント後に取引状態を即座に再計算していることが確認されました。
制限到達(Limit Reached)への遷移
5回目の取引が実行されると、残りの取引可能回数は0となり、システムはSTATE_LIMIT状態へ移行しました。ダッシュボードは直ちにマルーン色(赤系)へ変化し、状態はTRADING LIMIT REACHEDに更新されました。これは、1日の境界に到達したことを明確に示します。この遷移により、履歴スキャンによる正確な取引数カウント、取引日の開始時刻の正しい設定、制限到達時の即時的な状態再分類、ダッシュボードと共有ライブラリロジック間の完全な同期が確認されました。
1日の取引制限に到達すると、ブロック機構が有効になり、それ以降の取引実行を防止します。追加の取引試行自体は可能ですが、システムは直ちに介入し、それらをキャンセルまたは決済します。ダッシュボードは引き続きリアルタイムで更新され、試行された取引数を事前定義された閾値と比較して表示します。ただし、状態は赤色のまま維持され、それ以上の取引が禁止された状態であることを示します。
フェーズ2:制限違反後の強制
2番目の図は、重要な強制段階を示しています。

図4:制限到達後の強制
1日の取引制限に到達した後、手動で追加の取引を試みたにもかかわらず、システムは即座に制限を強制しました。新しい非成行注文はキャンセルされ、新しく建てられたポジションは遅延なく決済されました。一方で、制限有効化前にオープンされていたポジションには影響がありませんでした。これは、エンフォーサーEAが正しくOnTradeTransactionを通じて取引イベントを検出し、 処理実行前にForceRefresh()を使用して取引状態を更新し、 内部フラグによって制限状態への移行を正確に識別し、 必要なタイミングでクリーンアップ処理を実行したことを示します。取引試行から制限の強制までの間に確認可能な遅延は発生せず、既存ポジションに影響を与えることなく追加エクスポージャーが防止されました。
このテストにより、すべてのコンポーネントが統合された1つのシステムとして連携して動作することが確認されました。共有ライブラリは口座全体の取引を正確に計算し、どの銘柄や手動取引もカウントから除外されませんでした。ダッシュボードインジケータは状態遷移をリアルタイムで反映し、取引活動が発生すると即座に更新されました。エンフォーサーEAは、制限条件が満たされた場合に常に即座かつ一貫して応答し、信頼性の高い強制動作を示しました。ターミナルのグローバル変数は、すべてのコンポーネント間で設定情報を正常に同期し、単一の情報源を維持しました。重要な点として、手動による取引試行でも制限ロジックを回避することはできず、1日の制限を超えようとする複数回の試行後も、システムは一貫した動作を維持しました。
このライブデモ評価から、いくつかの重要な結論を導くことができます。取引数のカウントは、銘柄や執行方法に関係なく正確に維持されます。状態遷移は取引イベント後すぐに発生し、正しい再計算ロジックが確認されました。注意喚起レイヤーは、制限適用前の早期警告機能として効果的に動作します。1日の取引制限に到達すると、制限は自動的かつ例外なく強制されます。テスト中に競合状態やタイミングの不整合は確認されませんでした。全体として、このシステムは実際のチャート環境において、安定性と信頼性の高い動作を示しました。
結論
本記事では、1日の取引制限を一連の技術仕様として定義し、それをMT5における口座レベルの強制レイヤーとして実装しました。目的は単に警告を表示することではなく、明確に定義された条件下で、1取引日における事前設定された最大エントリー数を超えることができないことを保証することでした。
完成したパッケージは、以下の3つの連携するコンポーネントで構成されています。
- DailyTradeLimit.mqh:セッション時間管理、DEAL_ENTRY_INのカウント、状態管理を一元化する共有ライブラリ
- DailyTradeLimitDashboard.mq5:リアルタイムの状態表示とアラートを提供する設定・監視レイヤー
- DailyTradeLimitEnforcer.mq5:制限の介入と強制を担当する独立したEA
このシステムは、設定可能なセッション開始時間を基準として、1取引日ごとの確定したDEAL_ENTRY_INトランザクションをカウントします。対象範囲は口座全体であり、すべての銘柄を対象とします。また、手動取引および第三者のEAによる取引も含まれます。定義された閾値に到達すると、新しく配置された保留中の注文は削除され、制限到達後にオープンされたポジションは直ちに決済されます。制限到達前に保有していたポジションには影響を与えません。
検証は、構造化された実際の取引環境でのテストシナリオを通じて実施されました。対象には複数銘柄での取引実行、複数EAによる同時動作、制限到達後の繰り返しの手動エントリー試行、セッションリセットの検証が含まれます。すべてのケースにおいて、取引数のカウントは一貫して維持され、状態遷移は即座に発生し、制限を超過することはありませんでした。
最終的な成果は、明確に定義され、テスト可能な不変条件です。つまり、エントリーと取引日の定義が指定された場合、口座は設定された1日のエントリー数を超えて取引を実行しません。
今後の拡張では、ここで確立した仕様駆動型かつモジュール化された設計アプローチに従い、利益目標やドローダウン上限などの追加制約を組み込むことができます。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21313
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5取引ツールのアクセシビリティ課題を克服する(第2回):Pythonのテキスト読み上げエンジンでEAを音声対応にする
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
ラリー・ウィリアムズの『市場の秘密』(第13回):隠れスマッシュデー反転パターンの自動化
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索