English Русский Deutsch
preview
取引規律をコードに組み込む(第3回):MQL5でホワイトリストを用いて銘柄単位の取引境界を強制適用する

取引規律をコードに組み込む(第3回):MQL5でホワイトリストを用いて銘柄単位の取引境界を強制適用する

MetaTrader 5トレーディングシステム |
10 0
Christian Benjamin
Christian Benjamin

内容


はじめに

規律を重視するトレーダーであっても、必ずしも戦略そのものが原因ではなく、誤った銘柄を取引することで損失を発生させる場合があります。MetaTrader 5では、見慣れないチャートへエキスパートアドバイザー(EA)を適用する、検証済みではない銘柄へ手動で待機注文を設定する、あるいは対象銘柄を制限せずに同一戦略を複数の銘柄で実行するといった状況で、この問題が発生する可能性があります。したがって、エンジニアリング上の問題は明確です。手動取引とEA取引の両方において、明示的に承認されていない銘柄での注文実行または約定を防止することです。

本記事では、OnTradeTransactionを使用して、取引イベントレベルで銘柄単位の規律を強制するMQL5による実用的なソリューションを紹介します。このシステムは、承認済み銘柄のホワイトリストを中心に構築されており、許可された集合に含まれない銘柄に対して、待機注文の追加および市場取引を積極的にブロックします。ブロックされた各試行は透明性確保のためにログへ記録され、ダッシュボードでは設定管理および許可された銘柄のリアルタイム確認が可能です。

実際の運用では、このアプローチによって銘柄規律を裁量的なガイドラインから、取引環境自体によって強制される構造的なルールへ変換できます。どの銘柄を取引すべきかをトレーダーが記憶しておく必要はありません。その代わり、事前定義された銘柄集合のみが戦略実行へ参加できることをシステムが保証します。

以降のセクションでは、銘柄単位の取引制限という概念、そのソリューションのアーキテクチャ、そしてMQL5での実装について説明します。さらに、承認されていない銘柄での取引試行を確実にブロックしながら、設定されたホワイトリスト上では通常動作を許可する仕組みについて、テスト結果を提示します。


概念の理解

本連載「取引規律をコードに組み込む」の前回2回の記事では、規律を単なる意識や意図の段階から発展させ、取引システムへ直接組み込む方法について説明しました。これらの議論では、重要な現実を強調しました。それは、ルールを知っていることと、ルールを強制することは同じではないという点です。トレーダーはリスク管理の原則を十分理解していても、プレッシャーのかかる状況では、それらの原則に反する行動を取ることがあります。したがって、自動化の目的はトレーダーを置き換えることではありません。トレーダーの一貫性が失われることから、トレーダー自身を保護することです。

なぜ銘柄規律が重要なのか

規律が崩れやすい領域の1つが、銘柄選択です。多くのトレーダーは、まず自分が十分理解している特定の通貨ペアや金融商品から取引を始めます。時間をかけて、それらの銘柄が異なるセッションでどのように動くのか、ニュースへどのように反応するのか、どの程度のボラティリティを持つのか、そしてセットアップがどれほど明確に形成されるのかを分析します。このような銘柄への理解は、統計的な安心感と体系的な意思決定につながります。しかし、現在の取引プラットフォームでは、数十、場合によっては数百もの金融商品へアクセスできます。わずかなスクロール操作だけで、EURUSDからNASDAQへ、Goldからエキゾチック通貨ペアへ移動できます。

このアクセスの容易さは、誘惑を生みます。トレーダーは、衝動的に分散をおこなうことがあります。また、アルゴリズムトレーダーであっても、どの銘柄を許可するかを慎重に管理せず、複数のチャートへEAを適用する場合があります。最初は「探索」として始めた行動が、気付かないうちに一貫性の欠如へ変化する可能性があります。

時間の経過とともに、以下の問題につながります。

  • 戦略の逸脱:1つの市場構造向けに設計された戦略を、異なるボラティリティや流動性特性を持つ銘柄へ適用してしまうこと。
  • バックテスト比較におけるデータ汚染:元々の検証範囲に含まれていなかった銘柄の結果を混在させることで、パフォーマンス評価の信頼性が低下すること。
  • 心理的な一貫性の欠如:銘柄間で意識を切り替えることで、判断の明確性が低下し、感情的な意思決定が増加すること。
  • 意図した範囲を超えたリスク拡大:計画していなかった複数の銘柄を同時に取引することで、意図せずエクスポージャーが増加すること。

重要なのは、多くの場合、問題は戦略そのものではないという点です。トレーダーは、定義された条件下で有効に機能する、十分に検証された手法を持っている可能性があります。本当の弱点は、強制する仕組みが存在しないことにあります。ルールは存在しています。しかし、それが破られることを防ぐ仕組みはありません。

銘柄ホワイトリスト

このフレームワークで実装されるシステムは、ホワイトリストモデルに従っています。つまり、承認ベースの実行モデルとして動作します。禁止された金融商品の一覧を管理する独立したブラックリストコンポーネントは存在しません。その代わりに、このアーキテクチャでは、どの銘柄が実行を許可されているかを明示的に指定することで、管理された取引対象範囲を定義します。事前定義された集合に含まれない金融商品は、自動的に取引対象外として扱われます。

したがって、銘柄ホワイトリストとは、システム内で取引を承認された金融商品の正式に設定された集合です。これは実行ロジックへ直接統合された検証レイヤーとして機能します。注文要求が処理される前に、システムは現在の銘柄が承認済みリストに存在するかを確認します。この検証はプログラムによって一貫して実行され、例外はありません。

判定構造は二値的かつ決定論的です。

  • ホワイトリストに銘柄が存在する場合:実行を継続します。
  • ホワイトリストに銘柄が存在しない場合:実行は自動的に拒否されます。

条件付きの上書き、状況に応じた解釈、実行時の手動介入は存在しません。適格性チェックは機械的かつ絶対的に実行されます。

ホワイトリストとブラックリストの比較

このモデルをブラックリスト方式の設計と区別することは重要です。

  1. ホワイトリストモデルでは、許可される対象を明示的に定義し、それ以外をデフォルトで除外します。
  2. ブラックリストモデルでは、禁止される対象を明示的に定義し、それ以外の取引対象範囲へのアクセスを維持します。

このシステムは、完全にホワイトリストモデルに従っています。金融商品が拒否される理由は、禁止リストに存在するためではありません。承認済み集合の一部ではないためです。この制限は、明示的な禁止ではなく、除外によって暗黙的に実現されています。構造的な観点から見ると、この違いは重要です。ブラックリストでは、広範囲の取引を許可したまま、一部の銘柄だけを除外します。一方、ホワイトリストでは、閉じられた運用範囲を構築します。取引活動は、意図的に設定された範囲を超えて拡大することはできません。ブローカーが新しい金融商品を追加した場合や、プラットフォームが追加の銘柄を提供した場合でも、ホワイトリスト設定へ意図的に追加されない限り、それらは利用できません。

コードレベルでの強制が重要な理由

裁量取引環境では、銘柄選択が徐々に当初の設計範囲から逸脱する可能性があります。自動取引環境であっても、どの銘柄を許可するかを厳密に制御しなければ、EAが複数のチャートへ適用される場合があります。時間の経過とともに、これは戦略検証時には考慮されていなかった変動要因を生み出します。

実行パイプラインへホワイトリストモデルの検証を組み込むことで、金融商品の利用可否は変更可能な変数ではなく、管理されたパラメータになります。システムは、承認されていない銘柄上でセットアップが「有効に見えるか」を評価しません。単純に、実行前に承認状態を確認します。これにより、戦略ロジックは調査および検証された範囲内でのみ動作します。

ホワイトリストモデルの利点

取引インフラの一部として実装された場合、ホワイトリスト強制機能は、測定可能な運用上のメリットを提供します。

利点
運用上の影響
定義された取引対象範囲

実行対象を、元々調査および検証された銘柄に限定できます。

範囲拡大への保護
計画していなかった追加銘柄や新規上場銘柄の含有を防止します。
バックテストの整合性
意図しない銘柄によってパフォーマンス結果が歪められることを防ぎます。
エクスポージャー制御
市場への参加範囲を事前定義された境界内に維持します。
設定に対する説明責任
承認された金融商品の集合を透明化し、レビュー可能にします。
導入時の安全性
EAが未承認チャートへ適用された場合、実行をブロックします。
構造的ガバナンス
機関投資家向けのリスクフィルターに匹敵するルールベースの制御を導入します。
監査対応
拒否された銘柄を記録し、監視および評価を可能にします。

運用上の結果

銘柄レベルの強制が存在しない場合、金融商品の利用可否は流動的な状態になります。この流動性は、初期段階では問題がないように見えるかもしれません。しかし、それは実行データ、リスクエクスポージャー、パフォーマンス分析に一貫性の欠如をもたらします。ホワイトリストは、この曖昧さを排除し、利用可否を固定された設定パラメータへ変換します。システムは、自制心、判断力、記憶に依存しません。依存するのは検証ロジックです。承認チェックを通過した金融商品のみが、実行エンジンと連携することを許可されます。

システムアーキテクチャの概要

SymbolWhitelistEnforcerは、明確性、責任分離、長期的な保守性を考慮して設計された、モジュール型の3層システムとして構成されています。各コンポーネントは、それぞれ単一の明確な役割を担当します。一方、コンポーネント間の連携は、共有ファイルベースのストレージ機構を通じて処理されます。この設計により、設定、検証、強制適用、レポート処理が同期された状態を維持しながら、モジュール間の強い結合を発生させることなく運用できます。

アーキテクチャは以下の要素で構成されます。

  1. コアインクルードファイル:SymbolWhitelist.mqh
  2. ダッシュボードインジケータ:SymbolWhitelistDashboard.mq5
  3. エンフォーサーEA:SymbolWhitelistEnforcer.mq5

すべてのコンポーネントは、永続ストレージファイル(SymbolWhitelist.txtおよびSymbolWhitelistLog.csv)を介して連携します。これらのファイルは、システム全体の共有状態レイヤーとして機能します。

ファイルシステムは、各コンポーネント間を同期する中立的なレイヤーとして機能します。コンポーネント同士が直接通信することはありません。代わりに、それぞれがコアライブラリによって共有ストレージに対して読み書きをおこないます。これにより、相互依存関係を排除し、システムをシンプルで予測可能な状態に維持できます。

1. コアインクルードファイル:SymbolWhitelist.mqh

コアインクルードファイルは、システムの中心となるロジックエンジンとして機能します。これは実行可能なプログラムではなく、ダッシュボードとエンフォーサーEAの両方からインポートされる再利用可能なライブラリです。すべての検証ロジックおよびファイル操作ロジックを1か所へ集約することで、各モジュールが同一のルールを適用し、一貫したデータを利用して動作することを保証します。

その責務は、3つの機能領域に整理されています。

ホワイトリスト管理

このライブラリは、承認済み銘柄の永続管理を担当します。SaveWhitelist()は設定された銘柄リストをSymbolWhitelist.txtへ保存し、LoadWhitelist()は必要に応じてその内容を取得します。信頼性を確保するため、ParseWhitelist()は保存されたカンマ区切り文字列を適切にトリミングされた配列へ変換します。これにより、ユーザーによる入力形式に柔軟性を持たせながら、検証ロジックを壊すことなく処理できます。

銘柄検証

IsSymbolAllowed()は決定的な制御関数です。この関数は現在のホワイトリストを読み込み、解析した後、検証対象となる銘柄に対して大文字・小文字を区別しない比較を実行します。この関数は、エンフォーサーEAが制御処理を実行する前に呼び出されます。

ログ記録と監査

透明性を確保するため、LogBlockedAttempt()は拒否されたすべての取引試行をSymbolWhitelistLog.csvへ記録します。記録内容には、タイムスタンプ、銘柄、取引元(手動またはEA)が含まれます。ReadLog()は、ダッシュボード表示用に最新のログエントリを取得します。この仕組みにより、追跡可能性と運用上の説明責任が確立されます。

すべてのファイル操作は、このライブラリ内で一元管理され、エラーチェックも実行されます。そのため、このライブラリはシステム全体における信頼できる唯一の情報源として機能します。

2. ダッシュボードインジケータ:SymbolWhitelistDashboard.mq5

ダッシュボードは、設定管理とリアルタイムの可視化を提供します。取引処理へ干渉することはありません。その代わりに、システム状態をユーザーが確認可能な視覚形式へ変換します。初期化時、ダッシュボードはInpWhitelistパラメータを受け取り、コアインクルードファイルを使用して承認済み銘柄を共有ファイルへ書き込みます。この処理によって、システム全体の運用範囲が定義されます。

視覚的には、ダッシュボードは以下の情報を表示する構造化されたパネルを描画します。

  • 承認済み銘柄リスト
  • 現在のチャートにおける許可状態
  • 最近ブロックされた取引試行
  • 強制状態の確認

1秒間隔のタイマーによって、ホワイトリストおよびログデータを再読み込みし、表示内容を更新します。これにより、EAによって実行された強制適用処理が即座に画面へ反映されます。ダッシュボードの目的は明確性です。インターフェース自体へ制御ロジックを組み込むことなく、ルール設定を可視化し、強制適用処理を確認できるようにします。

3. エンフォーサーEA:SymbolWhitelistEnforcer.mq5

エンフォーサーEAは、制限を適用するアクティブな制御レイヤーです。ダッシュボードとは独立して動作し、監視と修正処理のみを担当します。初期化時、EAは有効化状態を確認した後、MetaTrader 5ターミナルによって生成されるOnTradeTransactionイベントの監視を開始します。これは特に、新規待機注文(TRADE_TRANSACTION_ORDER_ADD)と約定(TRADE_TRANSACTION_DEAL_ADD)を評価します。

各対象イベントに対して、EAは銘柄を取得し、コアインクルードファイルのIsSymbolAllowed()を呼び出します。銘柄が承認されている場合、処理は実行されません。

銘柄が承認されていない場合、EAは直ちに以下を実行します。

  • LogBlockedAttempt()を介して試行を記録する
  • 待機注文をキャンセル、または保有ポジションを決済する
  • エキスパートログに確認メッセージを書き込む

EAはイベント駆動型であり、ステートレスです。タイマーやバックグラウンドループには依存しません。取引イベントに即座に反応することで、迅速に強制できるようにします。

統合されたシステム動作

ワークフローは、構造化された手順に従って動作します。ホワイトリストはダッシュボードを通じて定義されます。コアライブラリは、保存処理と検証ロジックの標準化を担当します。エンフォーサーEAは取引活動を監視し、必要に応じて制限を適用します。ブロックされたすべての試行はログへ記録され、その後ダッシュボードへ表示されます。この分離構造により、設定、検証、強制適用、レポート処理は、それぞれ独立性を維持しながら同期して動作します。


MQL5での実装

アーキテクチャの基盤を説明したところで、ここからは概念を実際に動作するMQL5コードへ落とし込んでいきます。実装は、システム概要で説明したモジュール構成に対応しており、コアエンジン、ダッシュボードインジケータ、エンフォーサーEAの3つの独立したコンポーネントで構成されています。これらは相互に連携しながらも、それぞれ明確に役割が分離されています。各コンポーネントは、銘柄単位の取引規律という考え方を、実用的で自動化された仕組みへ変換する上で重要な役割を担います。このような構成を採用することで、システム全体の一貫性、保守性、そしてシームレスなユーザー体験を実現しています。

このセクションでは、単にコードを説明するだけではありません。各設計判断の背景にある考え方にも焦点を当て、規律をコードへ組み込むことが、どのように戦略の整合性を維持し、リスク管理を徹底し、取引判断への感情の影響を抑えるのかを説明します。

パート1:コアエンジン(SymbolWhitelist.mqh)

コアエンジンは、このシステムの中核となるロジックを担います。重要なロジックをすべて共通のインクルードファイルへ集約することで、ダッシュボードとエンフォーサーEAの両方が同じルールとデータに基づいて動作し、コードの重複や整合性の欠如を防ぐことができます。この設計は、ソフトウェアエンジニアリングにおける単一責任の原則を体現しています。1つのコンポーネントがロジックとデータ処理を担当し、それ以外のコンポーネントは表示または強制適用に専念します。

専用の名前空間SWLを使用することで、すべての関数を独立させ、他の取引ユーティリティとの名前衝突を防止しています。また、WHITELIST_FILEとLOG_FILEという2つの定数で、設定情報とイベントログを保存するテキストファイルおよびCSVファイルを定義しています。この設計により、実行時の動作との互換性を維持しながら、人が内容を確認・編集できる形式で設定やログを管理できます。

//+------------------------------------------------------------------+
//|                                               SymbolWhitelist.mqh|
//|                                   Copyright 2026, MetaQuotes Ltd.|
//|                           https://www.mql5.com/ja/users/lynnchris|
//+------------------------------------------------------------------+
namespace SWL
  {
const string WHITELIST_FILE = "SymbolWhitelist.txt";
const string LOG_FILE       = "SymbolWhitelistLog.csv";

ホワイトリストの保存:SaveWhitelist()

SaveWhitelist()は、許可された銘柄一覧をディスクへ保存します。この関数はファイルを書き込みモードで開き、銘柄をカンマ区切り文字列として保存した後、処理結果をbool値で返します。設定をコードから切り離して外部ファイルへ保存することで、静的なコードへの依存をなくし、動的な設定変更を可能にしています。許可対象の金融商品は、ファイルを直接編集するか、更新した入力パラメータでダッシュボードを再適用するだけで変更できます。コードを再コンパイルする必要はありません。

//+------------------------------------------------------------------+
//| Saves the whitelist string to file                               |
//+------------------------------------------------------------------+
bool SaveWhitelist(string whitelist)
  {
   int handle = FileOpen(WHITELIST_FILE,FILE_TXT|FILE_WRITE);
   if(handle==INVALID_HANDLE)
      return false;
   FileWrite(handle,whitelist);
   FileClose(handle);
   return true;
  }
//+------------------------------------------------------------------+

設定情報と実行処理を分離することで、SaveWhitelist()は更新内容をすべてのコンポーネント間で即座に共有し、信頼できる唯一の情報源を維持します。また、この方式は、複数ユーザーや複数戦略が共存する環境における自動化ワークフローにも適しています。

ホワイトリストの読み込み:LoadWhitelist()

LoadWhitelist()は、保存された設定を読み込むための対応する関数です。まずファイルの存在を確認し、存在しない場合は安全な初期値として空文字列を返します。ファイルが存在する場合は、その内容を読み込んで返します。この仕組みにより、ダッシュボードとEAは常に最新の設定を利用でき、初回実行時やファイルが誤って削除された場合でも適切に動作します。

//+------------------------------------------------------------------+
//| Loads the whitelist from file, returns as string                 |
//+------------------------------------------------------------------+
string LoadWhitelist()
  {
   if(!FileIsExist(WHITELIST_FILE))
      return "";
   int handle = FileOpen(WHITELIST_FILE,FILE_TXT|FILE_READ);
   if(handle==INVALID_HANDLE)
      return "";
   string data = FileReadString(handle);
   FileClose(handle);
   return data;
  }
//+------------------------------------------------------------------+

この関数は、手動取引環境と自動取引環境の両方において堅牢性を提供します。ファイルが存在しないことによる実行時エラーを防ぐことで、システムを導入した直後から安定して動作できるようにしています。

ホワイトリストの解析:ParseWhitelist()

ホワイトリストはカンマ区切りの単純な文字列として保存されますが、ParseWhitelist()はこれを銘柄ごとの配列へ変換します。この関数はStringSplit()を使用して各要素を分割し、StringTrimLeft()とStringTrimRight()によって前後の空白を除去します。空の要素は無視されるため、ユーザーの入力形式にかかわらず、一貫した配列を生成できます。

//+------------------------------------------------------------------+
//| Parses a comma‑separated whitelist into an array of symbols      |
//+------------------------------------------------------------------+
int ParseWhitelist(string list,string &result[])
  {
   ArrayResize(result,0);
   if(list=="")
      return 0;
   string parts[];
   int count = StringSplit(list,',',parts);
   for(int i=0; i<count; i++)
     {
      string trimmed = parts[i];
      StringTrimLeft(trimmed);
      StringTrimRight(trimmed);
      if(trimmed!="")
        {
         int sz = ArraySize(result);
         ArrayResize(result,sz+1);
         result[sz] = trimmed;
        }
     }
   return ArraySize(result);
  }
//+------------------------------------------------------------------+

例えば、「EURUSD、GBPUSD、XAUUSD」のように空白が不統一な入力であっても、ParseWhitelist()はこれを["EURUSD","GBPUSD","XAUUSD"]に標準化し、潜在的な不一致や適用エラーを防ぎます。これにより、不一致や制御エラーを防ぎながら、ユーザーにとって扱いやすい入力方法を維持できます。

銘柄が許可されているかの判定:IsSymbolAllowed()

IsSymbolAllowed()は、強制の中核となる判定処理を実装した関数です。この関数はホワイトリストを読み込み、解析した後、指定された銘柄に対して大文字・小文字を区別しない比較をおこないます。一致する銘柄が見つかればtrueを返し、それ以外の場合はfalseを返します。

//+------------------------------------------------------------------+
//| Checks if a given symbol is in the whitelist                     |
//+------------------------------------------------------------------+
bool IsSymbolAllowed(string symbol)
  {
   string list = LoadWhitelist();
   string allowed[];
   ParseWhitelist(list,allowed);
   for(int i=0; i<ArraySize(allowed); i++)
      if(StringCompare(allowed[i],symbol,false)==0)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

この判定処理を一元化することで、表示を担当するダッシュボードと、実際に取引をブロックするエンフォーサーは、常に同一のルールに従って動作します。大文字・小文字を区別しない比較はMetaTraderの慣例にも適合しており、意図しない不一致を防止します。この関数は、銘柄単位の取引規律を支える中核であり、抽象的なルールを実際に強制可能なロジックへ変換します。

ブロックされた取引の記録:LogBlockedAttempt()

取引がブロックされるたびに、LogBlockedAttemptは拒否された取引試行をSymbolWhitelistLog.csvへ記録します。記録内容には、タイムスタンプ、銘柄、および取引元(手動またはEA)が含まれます。この透明性の高い監査ログにより、トレーダーは強制履歴を確認し、衝動的な取引パターンを把握するとともに、システムが適切に機能していることを検証できます。

//+------------------------------------------------------------------+
//| Appends a blocked attempt to the log file                        |
//+------------------------------------------------------------------+
void LogBlockedAttempt(datetime time,string symbol,string source)
  {
   int handle = FileOpen(LOG_FILE,FILE_TXT|FILE_READ|FILE_WRITE|FILE_CSV,',');
   if(handle==INVALID_HANDLE)
      return;
   FileSeek(handle,0,SEEK_END);
   FileWrite(handle,TimeToString(time),symbol,source);
   FileClose(handle);
  }
//+------------------------------------------------------------------+

正確なログ記録は分析にも役立ちます。時間の経過とともに、許可されていない取引がどれだけ試行されたかを定量的に把握し、それを基に戦略を改善したり、追加の安全対策を自動化したりできます。

ログ履歴の読み取り:ReadLog()

ダッシュボードは、ReadLog()を使用して最新のブロック履歴を表示します。この関数はCSVファイルを読み込み、新しい順にエントリを返します。表示件数は設定可能な最大件数(デフォルトは10件)に制限されています。これにより、情報過多を防ぎながら、最新の制御状況を簡潔に表示できます。

//+------------------------------------------------------------------+
//| Reads the last N log entries (most recent first)                 |
//+------------------------------------------------------------------+
int ReadLog(string &times[],string &symbols[],string &sources[],int max=10)
  {
   ArrayResize(times,0);
   ArrayResize(symbols,0);
   ArrayResize(sources,0);
   if(!FileIsExist(LOG_FILE))
      return 0;

   int handle = FileOpen(LOG_FILE,FILE_TXT|FILE_READ|FILE_CSV,',');
   if(handle==INVALID_HANDLE)
      return 0;

   string tmpTime[], tmpSym[], tmpSrc[];
   while(!FileIsEnding(handle))
     {
      string t = FileReadString(handle);
      string s = FileReadString(handle);
      string src = FileReadString(handle);
      if(t!="" && s!="")
        {
         int sz = ArraySize(tmpTime);
         ArrayResize(tmpTime,sz+1);
         ArrayResize(tmpSym,sz+1);
         ArrayResize(tmpSrc,sz+1);
         tmpTime[sz] = t;
         tmpSym[sz] = s;
         tmpSrc[sz] = src;
        }
     }
   FileClose(handle);

   int total = ArraySize(tmpTime);
   int start = (total>max) ? total-max : 0;
   for(int i=total-1; i>=start; i--)
     {
      int sz = ArraySize(times);
      ArrayResize(times,sz+1);
      ArrayResize(symbols,sz+1);
      ArrayResize(sources,sz+1);
      times[sz] = tmpTime[i];
      symbols[sz] = tmpSym[i];
      sources[sz] = tmpSrc[i];
     }
   return ArraySize(times);
  }
  }
//+------------------------------------------------------------------+

パート2:ダッシュボードインジケータ(SymbolWhitelistDashboard.mq5)

ダッシュボードは、抽象的なルールをリアルタイムの可視化インターフェースへ変換する役割を担います。取引を実行することなく、システム全体の状態を一目で把握できるようにします。エンフォーサーから分離されているため、設計はシンプルかつモジュール化されています。トレーダーはダッシュボードを任意のチャートへ適用できるため、複数の戦略や複数の取引口座を組み合わせた柔軟な運用にも対応できます。

//+------------------------------------------------------------------+
//|                                      SymbolWhitelistDashboard.mq5|
//|                                   Copyright 2026, MetaQuotes Ltd.|
//|                           https://www.mql5.com/ja/users/lynnchris|
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Christian Benjamin"
#property link      "https://www.mql5.com/ja/users/lynnchris"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

#include <SymbolWhitelist.mqh>

//--- input parameters
input string InpWhitelist = "EURUSD,GBPUSD,XAUUSD";   // Allowed symbols (comma separated)
input int    InpX         = 20;                       // Panel X offset
input int    InpY         = 20;                       // Panel Y offset
input int    InpWidth     = 500;                      // Panel width
input int    InpHeight    = 300;                      // Panel height

//--- colors
input color  InpBgColor       = C'30,30,30';          // Background
input color  InpBorderColor   = C'80,80,80';          // Border
input color  InpChipBg        = C'70,70,70';          // Chip background
input color  InpChipBorder    = C'120,120,120';       // Chip border
input color  InpTextColor     = clrWhite;             // Text
input color  InpAllowedColor  = clrLimeGreen;         // Allowed indicator
input color  InpBlockedColor  = clrTomato;            // Blocked indicator
input color  InpActiveColor   = clrLimeGreen;         // Enforcement active

//--- global prefixes
string PREFIX = "SWL_";

初期化:OnInit()

適用時に、OnInit()はSWL::SaveWhitelist()を呼び出して入力ホワイトリストを共有ファイルへ書き込み、エンフォーサーとの整合性を確保します。その後、パネルを描画し、1秒間隔のタイマーを開始します。このタイマーによってライブ更新がおこなわれ、通常のチャート処理を妨げることなく、表示内容が動的に更新されます。

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Save initial whitelist to file
   SWL::SaveWhitelist(InpWhitelist);

   CreatePanel();
   EventSetTimer(1);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   DeleteObjectsByPrefix(PREFIX);
  }
//+------------------------------------------------------------------+

タイマーの更新:OnTimer()およびUpdateDashboard()

1秒ごとにタイマーがUpdateDashboard()を呼び出します。この関数はホワイトリストとログを再読み込みし、現在のシステム状態を反映するように表示要素を更新します。このライブフィードバックにより、どの取引が許可され、どの取引がブロックされているかを継続的に可視化し、トレード規律の維持を支援します。

//+------------------------------------------------------------------+
//| Custom indicator iteration function (required)                   |
//+------------------------------------------------------------------+
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[])
  {
   return(rates_total);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   UpdateDashboard();
  }
//+------------------------------------------------------------------+

パネルの描画:CreatePanel()

CreatePanel()は、見やすく直感的で、プロフェッショナルなユーザーインターフェースを構築します。表示の重複や不要なオブジェクトを防ぐため、既存のオブジェクトを削除した後、背景、セクション見出し、銘柄チップ、チャートステータス、ログエントリなどの動的要素用プレースホルダーを描画します。一貫した命名規則により、各オブジェクトを効率的かつ適切に管理できます。また、色分けされたステータス表示を採用することで、視認性を維持し、ライブトレード中の誤認や混乱を防ぎます。

//+------------------------------------------------------------------+
//| Creates the entire panel                                         |
//+------------------------------------------------------------------+
void CreatePanel()
  {
   DeleteObjectsByPrefix(PREFIX);

   //--- Background
   ObjectCreate(0,PREFIX+"BG",OBJ_RECTANGLE_LABEL,0,0,0);
   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_BGCOLOR,InpBgColor);
   ObjectSetInteger(0,PREFIX+"BG",OBJPROP_BORDER_TYPE,BORDER_FLAT);
   ObjectSetInteger(0,PREFIX+"BG",OBJPROP_COLOR,InpBorderColor);
   ObjectSetInteger(0,PREFIX+"BG",OBJPROP_BACK,false);

   //--- Header
   ObjectCreate(0,PREFIX+"HEADER",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"HEADER",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"HEADER",OBJPROP_YDISTANCE,InpY+8);
   ObjectSetString(0,PREFIX+"HEADER",OBJPROP_TEXT,"SYMBOL WHITELIST ENFORCER");
   ObjectSetInteger(0,PREFIX+"HEADER",OBJPROP_COLOR,InpTextColor);
   ObjectSetInteger(0,PREFIX+"HEADER",OBJPROP_FONTSIZE,12);
   ObjectSetInteger(0,PREFIX+"HEADER",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"HEADER",OBJPROP_SELECTABLE,false);

   //--- Separator line
   ObjectCreate(0,PREFIX+"SEP1",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"SEP1",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"SEP1",OBJPROP_YDISTANCE,InpY+30);
   ObjectSetString(0,PREFIX+"SEP1",OBJPROP_TEXT,"────────────────────");
   ObjectSetInteger(0,PREFIX+"SEP1",OBJPROP_COLOR,InpBorderColor);
   ObjectSetInteger(0,PREFIX+"SEP1",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"SEP1",OBJPROP_SELECTABLE,false);

   //--- Allowed symbols section header
   ObjectCreate(0,PREFIX+"ALLOWED_TITLE",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"ALLOWED_TITLE",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"ALLOWED_TITLE",OBJPROP_YDISTANCE,InpY+40);
   ObjectSetString(0,PREFIX+"ALLOWED_TITLE",OBJPROP_TEXT,"ALLOWED SYMBOLS");
   ObjectSetInteger(0,PREFIX+"ALLOWED_TITLE",OBJPROP_COLOR,InpTextColor);
   ObjectSetInteger(0,PREFIX+"ALLOWED_TITLE",OBJPROP_FONTSIZE,10);
   ObjectSetInteger(0,PREFIX+"ALLOWED_TITLE",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"ALLOWED_TITLE",OBJPROP_SELECTABLE,false);

   //--- Current chart section header
   ObjectCreate(0,PREFIX+"CURRENT_TITLE",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"CURRENT_TITLE",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"CURRENT_TITLE",OBJPROP_YDISTANCE,InpY+95);
   ObjectSetString(0,PREFIX+"CURRENT_TITLE",OBJPROP_TEXT,"CURRENT CHART");
   ObjectSetInteger(0,PREFIX+"CURRENT_TITLE",OBJPROP_COLOR,InpTextColor);
   ObjectSetInteger(0,PREFIX+"CURRENT_TITLE",OBJPROP_FONTSIZE,10);
   ObjectSetInteger(0,PREFIX+"CURRENT_TITLE",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"CURRENT_TITLE",OBJPROP_SELECTABLE,false);

   //--- Log section header
   ObjectCreate(0,PREFIX+"LOG_TITLE",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"LOG_TITLE",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"LOG_TITLE",OBJPROP_YDISTANCE,InpY+150);
   ObjectSetString(0,PREFIX+"LOG_TITLE",OBJPROP_TEXT,"TODAY'S BLOCKED ATTEMPTS");
   ObjectSetInteger(0,PREFIX+"LOG_TITLE",OBJPROP_COLOR,InpTextColor);
   ObjectSetInteger(0,PREFIX+"LOG_TITLE",OBJPROP_FONTSIZE,10);
   ObjectSetInteger(0,PREFIX+"LOG_TITLE",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"LOG_TITLE",OBJPROP_SELECTABLE,false);

   //--- Footer (enforcement status)
   ObjectCreate(0,PREFIX+"FOOTER",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"FOOTER",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"FOOTER",OBJPROP_YDISTANCE,InpY+InpHeight-25);
   ObjectSetInteger(0,PREFIX+"FOOTER",OBJPROP_COLOR,InpActiveColor);
   ObjectSetInteger(0,PREFIX+"FOOTER",OBJPROP_FONTSIZE,11);
   ObjectSetInteger(0,PREFIX+"FOOTER",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"FOOTER",OBJPROP_SELECTABLE,false);

   //--- Dynamic elements (chips, status, log lines)
   for(int i=0; i<10; i++)
     {
      string name = PREFIX+"CHIP_"+IntegerToString(i);
      ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0);
      ObjectSetInteger(0,name,OBJPROP_BGCOLOR,InpChipBg);
      ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
      ObjectSetInteger(0,name,OBJPROP_COLOR,InpChipBorder);
      ObjectSetInteger(0,name,OBJPROP_BACK,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);

      name = PREFIX+"CHIPTXT_"+IntegerToString(i);
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);
      ObjectSetInteger(0,name,OBJPROP_COLOR,InpTextColor);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
      ObjectSetInteger(0,name,OBJPROP_BACK,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);

      name = PREFIX+"LOG_"+IntegerToString(i);
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);
      ObjectSetInteger(0,name,OBJPROP_COLOR,InpTextColor);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
      ObjectSetString(0,name,OBJPROP_FONT,"Consolas");
      ObjectSetInteger(0,name,OBJPROP_BACK,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
     }

   //--- Status lines
   ObjectCreate(0,PREFIX+"STATUS_SYM",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"STATUS_SYM",OBJPROP_COLOR,InpTextColor);
   ObjectSetInteger(0,PREFIX+"STATUS_SYM",OBJPROP_FONTSIZE,10);
   ObjectSetInteger(0,PREFIX+"STATUS_SYM",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"STATUS_SYM",OBJPROP_SELECTABLE,false);

   ObjectCreate(0,PREFIX+"STATUS_IND",OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,PREFIX+"STATUS_IND",OBJPROP_FONTSIZE,10);
   ObjectSetInteger(0,PREFIX+"STATUS_IND",OBJPROP_BACK,false);
   ObjectSetInteger(0,PREFIX+"STATUS_IND",OBJPROP_SELECTABLE,false);

   UpdateDashboard();
  }
//+------------------------------------------------------------------+

許可銘柄の表示:チップ描画

許可された銘柄は、「チップ」と呼ばれる、銘柄名を表示した視覚的に区別しやすい角丸の矩形として表示されます。チップはパネルの幅に応じて自動的に折り返して配置されるため、ホワイトリスト全体を一目で確認できます。単純なテキスト一覧とは異なり、このグラフィカルな表示により認知負荷が軽減され、トレーダーは取引が許可されている銘柄を素早く把握できます。

//+------------------------------------------------------------------+
//| Updates all dynamic panel content                                |
//+------------------------------------------------------------------+
void UpdateDashboard()
  {
   string whitelist = SWL::LoadWhitelist();
   string allowed[];
   int num = SWL::ParseWhitelist(whitelist,allowed);

   //--- Hide all chips first
   for(int i=0; i<10; i++)
     {
      ObjectSetInteger(0,PREFIX+"CHIP_"+IntegerToString(i),OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,PREFIX+"CHIPTXT_"+IntegerToString(i),OBJPROP_HIDDEN,true);
     }

   //--- Draw chips
   int x = InpX + 10;
   int y = InpY + 55;
   int chipHeight = 22;
   int margin = 8;
   for(int i=0; i<num && i<10; i++)
     {
      string sym = allowed[i];
      int textWidth = StringLen(sym) * 7;
      int chipWidth = textWidth + 16;

      if(x + chipWidth > InpX + InpWidth - 10)
        {
         x = InpX + 10;
         y += chipHeight + 5;
        }

      string chipName = PREFIX+"CHIP_"+IntegerToString(i);
      ObjectSetInteger(0,chipName,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,chipName,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,chipName,OBJPROP_XSIZE,chipWidth);
      ObjectSetInteger(0,chipName,OBJPROP_YSIZE,chipHeight);
      ObjectSetInteger(0,chipName,OBJPROP_HIDDEN,false);

      string txtName = PREFIX+"CHIPTXT_"+IntegerToString(i);
      ObjectSetInteger(0,txtName,OBJPROP_XDISTANCE,x+8);
      ObjectSetInteger(0,txtName,OBJPROP_YDISTANCE,y+3);
      ObjectSetString(0,txtName,OBJPROP_TEXT,sym);
      ObjectSetInteger(0,txtName,OBJPROP_HIDDEN,false);

      x += chipWidth + margin;
     }

現在のチャート状況:動的フィードバック

ダッシュボードは、現在アタッチされているチャートの銘柄を確認し、緑色の「ALLOWED」または赤色の「BLOCKED」インジケータを表示します。表示位置は重なりを防ぐために動的に調整され、長い銘柄名は適切に省略表示されます。これにより、トレーダーは現在のチャートがホワイトリストに含まれているかを即座に確認でき、見落としや衝動的な判断によるミスを防止できます。

   //--- Current chart status with dynamic positioning
   string currSym = Symbol();
   bool allowedNow = SWL::IsSymbolAllowed(currSym);
   string statusText = allowedNow ? "ALLOWED" : "BLOCKED";
   color statusColor = allowedNow ? InpAllowedColor : InpBlockedColor;

   int statusWidth = StringLen(statusText) * 8;
   int statusX = InpX + InpWidth - statusWidth - 40;
   if(statusX < InpX + 10)
      statusX = InpX + 10;

   int maxSymbolWidth = statusX - (InpX + 10) - 10;
   int maxChars = maxSymbolWidth / 7;
   string prefix = "Symbol: ";
   int prefixLen = StringLen(prefix);
   int symLen = StringLen(currSym);

   string displaySym;
   if(prefixLen + symLen <= maxChars)
      displaySym = prefix + currSym;
   else
     {
      int availableForSym = maxChars - prefixLen - 3;
      if(availableForSym < 1)
         availableForSym = 1;
      string truncated = StringSubstr(currSym,0,availableForSym) + "...";
      displaySym = prefix + truncated;
     }

   ObjectSetInteger(0,PREFIX+"STATUS_SYM",OBJPROP_XDISTANCE,InpX+10);
   ObjectSetInteger(0,PREFIX+"STATUS_SYM",OBJPROP_YDISTANCE,InpY+110);
   ObjectSetString(0,PREFIX+"STATUS_SYM",OBJPROP_TEXT,displaySym);

   ObjectSetInteger(0,PREFIX+"STATUS_IND",OBJPROP_XDISTANCE,statusX);
   ObjectSetInteger(0,PREFIX+"STATUS_IND",OBJPROP_YDISTANCE,InpY+110);
   ObjectSetString(0,PREFIX+"STATUS_IND",OBJPROP_TEXT,statusText);
   ObjectSetInteger(0,PREFIX+"STATUS_IND",OBJPROP_COLOR,statusColor);

ログ表示:整列されたエントリ

ダッシュボードには、最近ブロックされた取引試行が、整列された等幅フォントのカラム形式で表示されます。各行には、時刻(HH:MM)、桁揃えされた銘柄名、および手動取引またはEAによる取引を示すアイコンが表示されます。この整理されたレイアウトにより、情報を迅速に把握でき、システムの透明性と説明責任を強化します。

   //--- Log entries with fixed‑width columns (monospaced font)
   string times[], symbols[], sources[];
   int logCount = SWL::ReadLog(times,symbols,sources,5);
   int logY = InpY + 165;
   for(int i=0; i<5; i++)
     {
      string name = PREFIX+"LOG_"+IntegerToString(i);
      if(i < logCount)
        {
         string timePart = StringSubstr(times[i],11,5);
         string symPart = symbols[i];
         string icon = (sources[i] == "manual" ? "M" : "EA");

         int symLen = StringLen(symPart);
         if(symLen < 30)
           {
            for(int j=symLen; j<30; j++)
               symPart += " ";
           }
         else if(symLen > 30)
               symPart = StringSubstr(symPart,0,30);

         string text = StringFormat("%s  %s  %s",timePart,symPart,icon);
         ObjectSetString(0,name,OBJPROP_TEXT,text);
         ObjectSetInteger(0,name,OBJPROP_XDISTANCE,InpX+10);
         ObjectSetInteger(0,name,OBJPROP_YDISTANCE,logY + i*16);
         ObjectSetInteger(0,name,OBJPROP_HIDDEN,false);
        }
      else
         ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
     }

   ObjectSetString(0,PREFIX+"FOOTER",OBJPROP_TEXT,"ENFORCEMENT: ● ACTIVE");
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Deletes all objects with a given prefix                          |
//+------------------------------------------------------------------+
void DeleteObjectsByPrefix(string prefix)
  {
   for(int i=ObjectsTotal(0)-1; i>=0; i--)
     {
      string name = ObjectName(0,i);
      if(StringFind(name,prefix) == 0)
         ObjectDelete(0,name);
     }
  }
//+------------------------------------------------------------------+

パート3:エンフォーサーEA (SymbolWhitelistEnforcer.mq5)

エンフォーサーEAは、銘柄単位の取引規律を維持するためのアクティブな監視役です。ダッシュボードとは異なり、自動的に介入し、ホワイトリスト外の銘柄で発生するあらゆる取引をブロックします。独立して動作することで、ダッシュボードが削除された場合やチャートが閉じられた場合でも、システムが確実に機能し続けることを保証します。

//+------------------------------------------------------------------+
//|                                      SymbolWhitelistEnforcer.mq5|
//|                                   Copyright 2026, MetaQuotes Ltd.|
//|                           https://www.mql5.com/ja/users/lynnchris|
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Christian Benjamin"
#property link      "https://www.mql5.com/ja/users/lynnchris"
#property version   "1.00"
#property strict
#property description "Blocks any trade on symbols not in the whitelist."

#include <SymbolWhitelist.mqh>

初期化:OnInit()

起動時に、EAは確認メッセージをログへ記録し、自動売買が有効になっていることを確認します。タイマーを設定することも可能ですが、主要な処理ロジックはイベント駆動型の監視に基づいており、過剰なCPUリソースを消費することなく、取引トランザクションに対して即座に応答できるよう設計されています。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("SymbolWhitelistEnforcer started. AutoTrading must be enabled.");
   EventSetTimer(1);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   Print("SymbolWhitelistEnforcer stopped.");
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Timer function (not strictly needed)                             |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Nothing to do here
  }
//+------------------------------------------------------------------+

トランザクションの監視:OnTradeTransaction()

//+------------------------------------------------------------------+
//| Trade transaction handler - block disallowed symbols             | 
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
   string symbol = "";
   ulong ticket = 0;
   bool isManual = false;

   if(trans.type == TRADE_TRANSACTION_ORDER_ADD)
     {
      ticket = trans.order;
      if(ticket == 0)
         return;
      if(!OrderSelect(ticket))
        {
         Print("Failed to select order ", ticket);
         return;
        }
      symbol = OrderGetString(ORDER_SYMBOL);
      isManual = (OrderGetInteger(ORDER_MAGIC) == 0);
     }
   else
      if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
        {
         ticket = trans.deal;
         if(ticket == 0)
            return;
         if(HistoryDealSelect(ticket))
           {
            symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
            long magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
            isManual = (magic == 0);
           }
        }
      else
         return;

   if(symbol == "")
      return;

   if(!SWL::IsSymbolAllowed(symbol))
     {
      string source = isManual ? "manual" : "EA";
      Print("Blocking ", source, " trade on disallowed symbol: ", symbol);
      SWL::LogBlockedAttempt(TimeCurrent(), symbol, source);

      if(trans.type == TRADE_TRANSACTION_ORDER_ADD)
        {
         MqlTradeRequest req = {};
         MqlTradeResult  res = {};
         req.action   = TRADE_ACTION_REMOVE;
         req.order    = ticket;
         req.comment  = "Symbol not whitelisted";
         if(!OrderSend(req, res))
            Print("Failed to cancel order ", ticket, ": ", res.comment);
        }
      else
         if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
           {
            ulong posTicket = HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
            if(posTicket == 0)
               return;
            if(!PositionSelectByTicket(posTicket))
              {
               Print("Position ", posTicket, " not found.");
               return;
              }

            MqlTradeRequest req = {};
            MqlTradeResult  res = {};
            req.action    = TRADE_ACTION_DEAL;
            req.position  = posTicket;
            req.symbol    = symbol;
            req.volume    = PositionGetDouble(POSITION_VOLUME);
            req.deviation = 5;
            req.comment   = "Symbol not whitelisted";

            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
              {
               req.price = SymbolInfoDouble(symbol, SYMBOL_BID);
               req.type  = ORDER_TYPE_SELL;
              }
            else
              {
               req.price = SymbolInfoDouble(symbol, SYMBOL_ASK);
               req.type  = ORDER_TYPE_BUY;
              }
            if(!OrderSend(req, res))
               Print("Failed to close position ", posTicket, ": ", res.comment);
           }
     }
  }
//+------------------------------------------------------------------+

EAは、TRADE_TRANSACTION_ORDER_ADDおよびTRADE_TRANSACTION_DEAL_ADDを監視します。各イベントが発生すると、銘柄を取得し、SWL::IsSymbolAllowed()を使用して許可状態を検証し、その結果に応じて処理を実行します。このイベント駆動型のアプローチにより、即時の強制適用が可能となり、衝動的な取引判断に対する堅牢な安全策として機能します。

取引ソースの特定:マジックナンバー

マジックナンバーを確認することで、EAは手動取引と自動取引を区別します。この情報は分析目的でログに記録され、どの取引がホワイトリスト違反となる可能性があったか、また自動売買戦略が口座ルールを遵守しているかをトレーダーが把握できるようにします。

許可されていない取引のブロック:即時対応

銘柄が許可されていない場合、EAはそのイベントをログへ記録し、待機注文を即座にキャンセルします。または、成行注文を反対方向に発注することで、保有中のポジションをクローズします。急速に変化する市場環境での約定成功率を高めるため、わずかな許容偏差を適用します。これにより、許可されていない取引が残存することを防ぎ、取引規律を個人の判断ではなく、システム構造として確立します。

実践的なワークフロー

適用後、ダッシュボードとEAはシームレスに連携して動作します。トレーダーは許可銘柄を設定し、リアルタイムの状態を監視しながら、エンフォーサーによってルール逸脱から保護されます。これにより、銘柄レベルの規律管理が完全に自動化され、行動上の要件がシステムレベルの特性へと変換されます。


テストと結果

MQL5による実装が完了した後の重要なステップは、実際の取引環境においてシステムがどの程度効果的に銘柄レベルの規律を維持できるかを評価することです。このセクションでは、評価方法、テストシナリオ、および観測された結果について説明し、強制適用メカニズムの信頼性と、戦略的整合性の維持に与える影響を検証します。

最初のテストでは、アタッチされたチャートの銘柄が許可されているか、または許可されていないかを、システムが正確に判定できるかを確認しました。これは基本的かつ重要な検証項目です。以降のすべての強制適用処理は、この判定結果に依存するためです。ダッシュボードとエンフォーサーを、許可銘柄および未許可銘柄を含む複数のチャートへ適用し、それぞれの応答を確認しました。

以下の図は、システムの動作結果を示しています。許可された銘柄が正しく識別され「ALLOWED」と表示されるケース、および許可されていない銘柄が即座に「BLOCKED」と判定されるケースを示しています。結果から、システムが許可された銘柄と未許可の銘柄を確実に区別でき、完全な取引制御を実現するための基盤が構築されていることが確認できます。

許可

ブロック

取引許可とブロック機能の検証

システムにおける最も重要なテストは、(1)ホワイトリストに登録された銘柄のみ取引を許可する、(2)ホワイトリストに含まれない銘柄での取引をブロックするという2つの主要機能を検証することでした。

以下の図では、チャート上で発生したすべての取引試行を可視化しています。許可されたホワイトリスト対象銘柄での取引は通常通り実行され、一方で未登録銘柄に対する取引試行はエンフォーサーによって検出され、ブロックされます。許可・ブロックを問わず、すべての取引試行が表示されるため、システムの動作全体を確認できます。

この可視化により、本ツールが意図した通りに機能していることが確認できます。ホワイトリスト登録銘柄は干渉を受けることなく処理され、それ以外の銘柄は確実に実行が阻止されます。すべての取引試行をチャート上に表示することで、システムは完全な透明性を提供し、銘柄レベルの規律を強化するとともに、取引活動が事前に定義された戦略上の範囲内で厳密に維持されることを保証します。


結論

MetaTrader 5環境において、銘柄規律を単なる意識や習慣ではなく、システム動作として実現するための目的を限定した強制適用メカニズムを実装しました。本システムは、取引活動を事前に定義された許可銘柄(ホワイトリスト)上のみに制限し、未知の銘柄や意図しない銘柄での誤発注から取引戦略を保護します。

実装は、相互に連携する3つのMQL5コンポーネントで構成されています。共有インクルードモジュールは、ホワイトリスト設定の管理、解析、検証ロジック、およびイベントログ記録を担当します。ダッシュボードインジケータは、許可銘柄や最近ブロックされた取引試行を表示することで、視覚的なフィードバックを提供します。最後に、エンフォーサーEAはOnTradeTransactionを通じて取引イベントを監視し、許可されていない銘柄で実行された待機注文を即座にキャンセル、またはポジションを決済します。

テストの結果、本メカニズムが定義された制限を一貫して適用できることが確認されました。ホワイトリストに登録された銘柄での注文は通常通り処理される一方で、許可されていない銘柄への取引試行は検出され、遮断・削除され、システムログへ記録されます。

取引処理パイプラインに直接検証機能を組み込むことで、本システムは銘柄規律を行動上の習慣からプログラムによる制約へ変換します。その結果、取引戦略は意図された銘柄範囲内でのみ動作し、対象銘柄の拡張についても明示的かつ管理された設定変更として実施されます。

添付ファイル

ファイル名 説明
SymbolWhitelist.mqh  コアエンジン/共有インクルードファイル。ホワイトリスト管理に必要なすべての主要ロジック(SaveWhitelistLoadWhitelistParseWhitelist)、銘柄検証(IsSymbolAllowed)、およびログ・監査機能(LogBlockedAttemptReadLog)を含みます。本システムにおける信頼できる唯一の情報源として機能します。
SymbolWhitelistDashboard.mq5

ダッシュボードインジケータ。設定管理とリアルタイム可視化を提供します。ホワイトリスト入力を受け取り、ファイルへ保存するとともに、許可銘柄、現在のチャート許可状態、最近ブロックされた取引試行、強制状態を表示する構造化パネルを生成します。1秒間隔のタイマーによって更新されます。
SymbolWhitelistEnforcer.mq5  エンフォーサーEA。取引トランザクション(OnTradeTransaction)を監視するアクティブな制御レイヤーとして機能します。新規待機注文および約定を検出し、銘柄をホワイトリストと照合します。許可されていない銘柄での取引を自動的にキャンセルまたはクローズし、ブロックされたすべての試行を記録します。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21493

MQL5取引ツール(第22回):二項分布のヒストグラムと確率質量関数(PMF)の描画 MQL5取引ツール(第22回):二項分布のヒストグラムと確率質量関数(PMF)の描画
シミュレーション結果のヒストグラムと理論的な確率質量関数(PMF)を組み合わせた、二項分布を可視化するインタラクティブなMQL5プロットを開発します。本ツールでは、平均、標準偏差、歪度、尖度、パーセンタイル、信頼区間を実装し、さらに設定可能なテーマやラベル表示にも対応しています。また、ドラッグ操作、サイズ変更、リアルタイムでのパラメータ変更をサポートします。このツールを使用することで、トレード戦略の検証時に、期待される勝ち回数、起こり得るドローダウンの範囲、信頼区間を評価できます。
MQL5標準ライブラリエクスプローラー(第9回):ALGLIBによる多発するMAクロスオーバーシグナルのフィルタ処理 MQL5標準ライブラリエクスプローラー(第9回):ALGLIBによる多発するMAクロスオーバーシグナルのフィルタ処理
横ばいの価格変動中、トレーダーは複数の移動平均クロスオーバーによって過剰なシグナルに直面します。今回は、ALGLIBがどのように生の価格データを前処理し、フィルタ処理済みのクロスオーバー系列を生成するのかについて説明します。また、これらのレイヤーは発生時にアラートを生成することもできます。数学ライブラリをMQL5プログラムでどう活用できるのかを一緒に見ていきましょう。
KnitPkgを用いたMQL5向けパッケージベース開発手法 KnitPkgを用いたMQL5向けパッケージベース開発手法
MetaTrader向けにMQLを用いた高い信頼性と生産性を備えた開発を実現するため、MQL5/MQL4向けのプロジェクトマネージャであるKnitPkgを利用した、再利用可能な「パッケージ」を中心とする開発手法を紹介します。パッケージは、他のパッケージを構成する土台として利用できるだけでなく、EAやインジケータなど、MetaTraderプラットフォーム上で直接動作する最終成果物の基盤としても活用できます。
ダイナミックマルチペアEAの形成(第7回):リアルタイムトレードフィルタリングのためのクロスペア相関マッピング ダイナミックマルチペアEAの形成(第7回):リアルタイムトレードフィルタリングのためのクロスペア相関マッピング
このパートでは、複数銘柄型エキスパートアドバイザー(EA)にリアルタイム相関行列を統合し、冗長な取引やリスクが積み重なるポジションを防ぐ仕組みを構築します。複数ペア間の相互関係を動的に測定することで、EAは既存エクスポージャーと競合するエントリーをフィルタリングし、ポートフォリオ全体のバランスを改善します。その結果、システミックリスクを低減し、より質の高いトレード判断を実現します。