English Русский Deutsch
preview
ダイナミックマルチペアEAの形成(第6回):高頻度銘柄切り替えのための適応型スプレッド感度制御

ダイナミックマルチペアEAの形成(第6回):高頻度銘柄切り替えのための適応型スプレッド感度制御

MetaTrader 5 |
25 0
Hlomohang John Borotho
Hlomohang John Borotho
目次
  1. はじめに
  2. システム概要と戦略的アプローチ
  3. 導入手順
  4. バックテスト結果
  5. 結論


はじめに

本連載第5回では、異なる市場環境に応じた取引スタイルの選択という課題に取り組みました。具体的には、スキャルピングとスイングトレードを切り替えるシステムの構築について解説しました。スキャルピングは短い時間足で小さな値動きを捉えることに集中し、スイングトレードはより長い時間足で大きなトレンド方向の値動きを狙う手法であることを説明し、EAが市場コンテキストに応じてロジック、ストップ設定、時間軸を動的に適応させる方法を示しました。

これに対し、第6回では、特定のエントリーおよびエグジット手法そのものではなく、取引の実行環境、特にスプレッドとして表現される取引コストに焦点を当てます。本パートでは、全銘柄に対してリアルタイムのスプレッド状況を継続的に監視し、動的な感度しきい値を用いて現時点で最も取引に適した銘柄を判定するモジュールを導入します。このスプレッド評価に基づく高頻度な銘柄切り替えは、マルチペア構造全体を補完し、戦略ロジックよりも取引品質およびコスト効率を優先する設計となっています。


システム概要と戦略的アプローチ

適応型スプレッド感度EA (Spread Sensitivity EA)は、複数の金融商品にわたる取引執行を動的に最適化するよう設計された、高度なマルチ銘柄取引システムです。その中核では、すべての設定銘柄のリアルタイムスプレッドを継続的に監視し、コスト効率指標に基づいてランク付けすることで、最も有利な執行条件を持つ銘柄を優先的に取引します。

銘柄ランキングメカニズム

従来の単一銘柄EAとは異なり、本システムはインテリジェントなスプレッドフィルタリングを実装しています。これにより、スプレッドが異常に拡大している銘柄は一時的に無効化され、高コストなエントリーを不利な流動性状況下でおこなうことを防ぎます。一方で、スプレッドが正常に戻った場合には自動的に再び有効化されます。この適応型アーキテクチャにより、EAは市場のマイクロストラクチャの変化に応じて利用可能な銘柄間を動的に切り替えるスマートルーティングのような役割を果たします。その結果、常に最もコスト効率の良い銘柄で取引が実行されるようになります。

スプレッド保護機構

本取引戦略は、デュアル移動平均クロスオーバーとRSIモメンタム確認を組み合わせた、シンプルながら効果的なテクニカル分析手法を採用しています。スプレッド条件が良好な場合、システムはシグナルを生成します。短期指数移動平均が長期指数移動平均を上抜けし、かつRSIが売られ過ぎ圏にある場合には買いシグナルを生成し、短期指数移動平均が長期指数移動平均を下抜けし、かつRSIが買われ過ぎ圏にある場合には売りシグナルを生成します。この組み合わせにより、移動平均によってトレンド方向を捉えつつ、RSIによってエントリー精度を補強することで、バランスの取れた取引タイミングが実現されます。

リアルタイムダッシュボード


導入手順

//+------------------------------------------------------------------+
//|                                           Spread Sensitivity.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/ja/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/ja/users/johnhlomohang/"
#property version   "1.00"
#property description "Multi-Symbol EA with Adaptive Spread Sensitivity"
#property description "Dynamically switches between symbols based on spread efficiency"

#include <Trade/Trade.mqh>
#include <Trade/PositionInfo.mqh>

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input string   TradePairs = "EURUSD,GBPUSD,XAUUSD,US100,BTCUSD"; // Trading Pairs (comma separated)
input double   RiskPerTrade = 0.01;          // Risk % per trade
input int      MagicNumber = 98765;          // Magic Number

// Spread Sensitivity Settings
input group    "=== Spread Filter Settings ==="
input double   MaxAbsoluteSpread = 10.0;     // Max absolute spread (pips)
input double   MaxSpreadATRRatio = 0.25;     // Max spread/ATR ratio
input bool     UseAdaptiveFilter = true;     // Enable adaptive filtering
input int      DisableTimeoutSec = 60;       // Disable symbol timeout (seconds)
input bool     EnableSpreadRanking = true;   // Enable symbol ranking by spread
input int      MaxActiveSymbols = 3;         // Maximum active symbols at once

// Trading Strategy Settings
input group    "=== Trading Strategy Settings ==="
input ENUM_TIMEFRAMES TradingTimeframe = PERIOD_M5;  // Trading timeframe
input int      EMA_Fast_Period = 9;          // Fast EMA period
input int      EMA_Slow_Period = 21;         // Slow EMA period
input int      RSI_Period = 14;              // RSI period
input double   RSI_Overbought = 70;          // RSI overbought level
input double   RSI_Oversold = 30;            // RSI oversold level
input int      StopLoss_Pips = 30;           // Stop Loss in pips
input int      TakeProfit_Pips = 60;         // Take Profit in pips
input int      MaxOpenPositions = 1;         // Max positions per symbol
input int      TradeCooldownSeconds = 300;   // Cooldown between trades (seconds)

// ATR Settings for adaptive SL/TP
input group    "=== ATR Settings (Optional) ==="
input bool     UseATR_SL_TP = false;         // Use ATR for dynamic SL/TP
input double   ATR_SL_Multiplier = 1.5;      // ATR multiplier for SL
input double   ATR_TP_Multiplier = 2.0;      // ATR multiplier for TP

// Dashboard Settings
input group    "=== Dashboard Settings ==="
input bool     ShowDashboard = true;         // Show dashboard on chart
input color    DashboardBGColor = clrBlack;  // Dashboard background color
input color    DashboardTextColor = clrWhite;// Dashboard text color
input int      DashboardX = 20;              // Dashboard X position
input int      DashboardY = 20;              // Dashboard Y position
input int      FontSize = 8;                 // Dashboard font size

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
string   SymbolList[];
int      TotalPairs;
CTrade   Trade;
CPositionInfo PositionInfo;
datetime LastDashboardUpdate = 0;

// Spread monitoring structure
struct SpreadData
{
   string      symbol;
   double      spreadInPips;
   double      atrValue;
   double      spreadATRRatio;
   double      spreadScore;
   datetime    disabledUntil;
   bool        isTradeable;
   bool        isActive;
   datetime    lastTradeTime;
   int         tradeAttempts;
   int         successfulTrades;
   color       statusColor;
};

SpreadData spreadData[];

// Dashboard messages
string DashboardMessages[10];

本システムの開始にあたり、動的マルチペアエキスパートアドバイザー(EA)のための柔軟な設定レイヤーを構築します。ユーザー定義のすべての入力値を、整理されたモジュール構造としてクリーンかつ体系的にグループ化することで設計します。まず、一般的な取引制御として、監視対象銘柄のリスト、1取引あたりのリスク、ポジション追跡用のマジックナンバーなどを設定します。そこから、このEAの実行ロジックの中核となる専用のスプレッド感度セクションを導入します。これらの入力値は、絶対的および適応的なスプレッド上限、ATRで正規化されたスプレッドしきい値、一時的な銘柄無効化、および銘柄ランキング制約を定義します。これによりEAは、任意の時点においてどの市場が取引対象としてコスト効率的であるかをインテリジェントに判断できるようになります。重要なのは、このレイヤーが取引のエントリー方法そのものを定義するものではなく、どの銘柄を売買候補として扱うかを決定する点にあるということです。これにより、マルチ銘柄における取引の実行品質が確保されます。

さらに進むと、コードは銘柄がスプレッドフィルターを通過した後にのみ動作する戦略レベルの入力および補助インフラを定義します。取引設定では、インジケーターパラメータ(EMA、RSI)、リスク境界(SL/TP、クールダウン、最大ポジション数)、および任意のATRベースの動的エグジットを構成し、制御された一貫性のある取引実行を可能にします。入力の下では、グローバル変数およびSpreadData構造体がEAの内部状態管理の中核を形成し、リアルタイムのスプレッド指標、銘柄状態、アクティブフラグ、取引統計、ダッシュボード表示を追跡します。この構造により、EAは銘柄のランキング、一時的な無効化および再有効化、そしてシステム挙動のリアルタイムダッシュボード表示を可能にし、ロジック面では適応性が高く、運用面でも透明性を確保しています。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   // Split trading pairs
   SplitString(TradePairs, ",", SymbolList);
   TotalPairs = ArraySize(SymbolList);
   
   if(TotalPairs == 0)
   {
      Print("Error: No symbols specified");
      return INIT_FAILED;
   }
   
   // Initialize spread data array
   ArrayResize(spreadData, TotalPairs);
   
   // Initialize dashboard messages
   for(int i = 0; i < 10; i++) DashboardMessages[i] = "";
   
   // Initialize each symbol
   for(int i = 0; i < TotalPairs; i++)
   {
      string symbol = SymbolList[i];
      
      // Validate symbol
      if(!SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE))
      {
         Print("Warning: Symbol ", symbol, " is not available for trading");
         continue;
      }
      
      // Initialize spread data
      spreadData[i].symbol = symbol;
      spreadData[i].spreadInPips = 0;
      spreadData[i].atrValue = 0;
      spreadData[i].spreadATRRatio = 0;
      spreadData[i].spreadScore = 0;
      spreadData[i].disabledUntil = 0;
      spreadData[i].isTradeable = true;
      spreadData[i].isActive = true;
      spreadData[i].lastTradeTime = 0;
      spreadData[i].tradeAttempts = 0;
      spreadData[i].successfulTrades = 0;
      spreadData[i].statusColor = clrGreen;
      
      // Subscribe to symbol
      SymbolSelect(symbol, true);
   }
   
   // Set trade parameters
   Trade.SetExpertMagicNumber(MagicNumber);
   Trade.SetDeviationInPoints(10);
   
   // Initialize dashboard
   if(ShowDashboard) CreateDashboard();
   
   AddDashboardMessage("EA Initialized with " + IntegerToString(TotalPairs) + " symbols");
   Print("EA Initialized. Total pairs: ", TotalPairs);
   
   return INIT_SUCCEEDED;
}

OnInit()関数は、取引開始前に銘柄、内部データ構造、実行設定を構成することで、EA全体の起動準備を担当します。まずユーザー定義の銘柄リストを解析し、少なくとも1つの取引可能な銘柄が存在するかを検証します。もし該当する銘柄が存在しない場合は、安全に初期化処理を終了します。次に、各銘柄に対してspreadData構造体を割り当てて初期化し、スプレッド指標、取引状態、統計情報、視覚的ステータス表示などのデフォルト値を設定します。同時に、各銘柄をリアルタイムデータ配信の対象として登録します。その後、マジックナンバーやスリッページ許容値といった取引実行パラメータを設定します。さらに、オンチャートダッシュボードが有効な場合はその初期化を行い、起動メッセージをログに出力し、正常に初期化が完了したことを確認します。これによりEAは、完全に同期され、監視され、適応的なマルチ銘柄運用が可能な状態で起動します。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // Remove dashboard objects
   if(ShowDashboard) RemoveDashboard();
   
   // Print statistics
   Print("=== Trading Statistics ===");
   for(int i = 0; i < TotalPairs; i++)
   {
      Print(spreadData[i].symbol, ": ", 
            spreadData[i].tradeAttempts, " attempts, ", 
            spreadData[i].successfulTrades, " successful trades");
   }
   
   Print("EA Deinitialized");
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   static int tickCounter = 0;
   tickCounter++;
   
   // Process one symbol per tick (prevents overloading)
   int symbolIndex = tickCounter % TotalPairs;
   string symbol = spreadData[symbolIndex].symbol;
   
   // Update spread data
   UpdateSpreadData(symbolIndex);
   
   // Check if symbol is tradeable
   if(!spreadData[symbolIndex].isTradeable || !spreadData[symbolIndex].isActive) 
      return;
   
   // Check cooldown period
   if(TimeCurrent() - spreadData[symbolIndex].lastTradeTime < TradeCooldownSeconds)
      return;
   
   // Execute trading logic
   ExecuteTradingLogic(symbolIndex);
   
   // Update dashboard every 10 ticks
   if(ShowDashboard && (tickCounter % 10 == 0) && (TimeCurrent() - LastDashboardUpdate >= 1))
   {
      UpdateDashboard();
      LastDashboardUpdate = TimeCurrent();
   }
}

OnDeinit()関数は、EAのクリーンかつ分かりやすい終了処理を保証します。EAが削除された場合やターミナルが閉じられた場合、この関数はまずチャート上のすべてのダッシュボードオブジェクトを削除し、残存する表示要素が残らないようにします。その後、各銘柄ごとの取引パフォーマンスの簡潔なサマリーを出力し、実行中に記録された取引試行回数と成功した取引数を報告します。この最終ログは透明性と実行後の診断性を提供し、EAが完全に終了する前に銘柄レベルでの挙動やシステムの有効性を評価しやすくします。

一方でOnTick()関数は、EAのリアルタイム動作フローを定義しており、マルチ銘柄環境における効率性を重視して設計されています。すべての銘柄を毎ティック処理するのではなく、モジュロカウンタを用いてティックごとに1つの銘柄のみを順番に処理することで、CPU負荷を軽減し、実行ボトルネックを回避します。選択された各銘柄について、EAはスプレッドデータを更新し、取引可能性を検証し、クールダウン制約を適用した上で、すべての条件が満たされている場合にのみ取引ロジックを実行します。ダッシュボードの更新は毎ティックではなく定期的に行われるよう制御されており、インターフェースの応答性を維持しつつ、高頻度な銘柄監視環境においてもパフォーマンスの安定性を確保します。

//+------------------------------------------------------------------+
//| Timer function for spread updates                                |
//+------------------------------------------------------------------+
void OnTimer()
{
   // Update all spread data and rank symbols
   UpdateAllSpreadData();
   if(EnableSpreadRanking) RankSymbolsBySpread();
}

//+------------------------------------------------------------------+
//| Update spread data for all symbols                               |
//+------------------------------------------------------------------+
void UpdateAllSpreadData()
{
   for(int i = 0; i < TotalPairs; i++)
   {
      UpdateSpreadData(i);
   }
}

//+------------------------------------------------------------------+
//| Update spread data for specific symbol                           |
//+------------------------------------------------------------------+
void UpdateSpreadData(int index)
{
   string symbol = spreadData[index].symbol;
   
   // Get current bid and ask
   double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
   
   if(bid == 0 || ask == 0) return;
   
   // Calculate spread in pips
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   double spreadPoints = (ask - bid) / point;
   spreadData[index].spreadInPips = NormalizeDouble(spreadPoints / 10, 1);
   
   // Calculate ATR for spread ratio
   spreadData[index].atrValue = CalculateATR(symbol, PERIOD_H1, 14);
   
   // Calculate spread/ATR ratio
   if(spreadData[index].atrValue > 0)
   {
      spreadData[index].spreadATRRatio = NormalizeDouble(spreadData[index].spreadInPips / spreadData[index].atrValue, 3);
   }
   
   // Evaluate tradeability
   EvaluateTradeability(index);
}

//+------------------------------------------------------------------+
//| Evaluate if symbol is tradeable based on spread                  |
//+------------------------------------------------------------------+
void EvaluateTradeability(int index)
{
   // Check if symbol is in timeout
   if(TimeCurrent() < spreadData[index].disabledUntil)
   {
      spreadData[index].isTradeable = false;
      spreadData[index].statusColor = clrOrange;
      return;
   }
   
   // Check absolute spread limit
   if(spreadData[index].spreadInPips > MaxAbsoluteSpread)
   {
      spreadData[index].isTradeable = false;
      spreadData[index].disabledUntil = TimeCurrent() + DisableTimeoutSec;
      AddDashboardMessage(spreadData[index].symbol + " disabled: High spread " + 
                          DoubleToString(spreadData[index].spreadInPips, 1));
      spreadData[index].statusColor = clrRed;
      return;
   }
   
   // Check ATR ratio if adaptive filtering is enabled
   if(UseAdaptiveFilter && spreadData[index].spreadATRRatio > MaxSpreadATRRatio)
   {
      spreadData[index].isTradeable = false;
      spreadData[index].statusColor = clrRed;
      return;
   }
   
   // All checks passed
   spreadData[index].isTradeable = true;
   spreadData[index].statusColor = clrGreen;
}

このブロックでは、ティック頻度とは独立して動作するタイマー駆動型のスプレッド監視システムを導入し、すべての銘柄の一貫性のあるタイムリーな評価を保証します。OnTimer()関数はスケジューラとして機能し、設定されたすべての銘柄のスプレッドデータを定期的に更新し、必要に応じてスプレッドの効率性に基づいてランク付けします。この設計では、スプレッド分析と価格ティックを切り離すことで、流動性が低い期間でもEAが応答性を維持できるようにしています。更新処理はUpdateAllSpreadData()からUpdateSpreadData()へと進みます。そこで Bid/Askを取得し、スプレッドをピップ単位で算出したうえで、ATR によりボラティリティの文脈を加えます。これにより、適応型スプレッド評価の基礎が形成されます。

EvaluateTradeability()関数は、階層的な意思決定プロセスを適用して、各銘柄が取引に適しているかどうかを判断します。まず、以前に無効化された銘柄に対してクールダウンタイムアウトを適用し、不安定な状況下での性急な再エントリーを防ぎます。次に、絶対スプレッド制限と適応型ATR正規化しきい値をチェックし、取引コストが高すぎる銘柄を自動的に無効化し、透明性を確保するためにこれらのイベントをダッシュボードに記録します。スプレッドのすべての条件が満たされると、その銘柄は取引可能とマークされ、正常状態として表示されます。これにより、静的なルールではなく、リアルタイムの約定品質に基づいて銘柄を動的にフィルタリングする、堅牢な保護メカニズムが完成します。

//+------------------------------------------------------------------+
//| Rank symbols by spread efficiency                                |
//+------------------------------------------------------------------+
void RankSymbolsBySpread()
{
   // Calculate spread score for each symbol
   for(int i = 0; i < TotalPairs; i++)
   {
      // Lower spread = better score
      double spreadComponent = 1.0 / (1.0 + spreadData[i].spreadInPips);
      
      // Lower ATR ratio = better score
      double atrComponent = 1.0 / (1.0 + spreadData[i].spreadATRRatio);
      
      // Combine components with weights
      spreadData[i].spreadScore = (0.6 * spreadComponent) + (0.4 * atrComponent);
   }
   
   // Simple bubble sort by score
   for(int i = 0; i < TotalPairs - 1; i++)
   {
      for(int j = i + 1; j < TotalPairs; j++)
      {
         if(spreadData[j].spreadScore > spreadData[i].spreadScore)
         {
            SpreadData temp = spreadData[i];
            spreadData[i] = spreadData[j];
            spreadData[j] = temp;
         }
      }
   }
   
   // Activate top N symbols
   for(int i = 0; i < TotalPairs; i++)
   {
      spreadData[i].isActive = (i < MaxActiveSymbols);
   }
}

//+------------------------------------------------------------------+
//| Execute trading logic for symbol                                 |
//+------------------------------------------------------------------+
void ExecuteTradingLogic(int index)
{
   string symbol = spreadData[index].symbol;
   
   // Check if symbol already has max positions
   if(CountOpenPositions(symbol) >= MaxOpenPositions) return;
   
   // Get indicator handles
   int handleEmaFast = iMA(symbol, TradingTimeframe, EMA_Fast_Period, 0, MODE_EMA, PRICE_CLOSE);
   int handleEmaSlow = iMA(symbol, TradingTimeframe, EMA_Slow_Period, 0, MODE_EMA, PRICE_CLOSE);
   int handleRSI = iRSI(symbol, TradingTimeframe, RSI_Period, PRICE_CLOSE);
   
   if(handleEmaFast == INVALID_HANDLE || handleEmaSlow == INVALID_HANDLE || handleRSI == INVALID_HANDLE)
   {
      Print("Error: Failed to create indicator handles for ", symbol);
      return;
   }
   
   // Get indicator values
   double emaFast[1], emaSlow[1], rsi[1];
   
   if(CopyBuffer(handleEmaFast, 0, 0, 1, emaFast) < 1) 
   {
      IndicatorRelease(handleEmaFast);
      IndicatorRelease(handleEmaSlow);
      IndicatorRelease(handleRSI);
      return;
   }
   
   if(CopyBuffer(handleEmaSlow, 0, 0, 1, emaSlow) < 1) 
   {
      IndicatorRelease(handleEmaFast);
      IndicatorRelease(handleEmaSlow);
      IndicatorRelease(handleRSI);
      return;
   }
   
   if(CopyBuffer(handleRSI, 0, 0, 1, rsi) < 1) 
   {
      IndicatorRelease(handleEmaFast);
      IndicatorRelease(handleEmaSlow);
      IndicatorRelease(handleRSI);
      return;
   }
   
   // Release indicator handles
   IndicatorRelease(handleEmaFast);
   IndicatorRelease(handleEmaSlow);
   IndicatorRelease(handleRSI);
   
   double emaF = emaFast[0];
   double emaS = emaSlow[0];
   double rsiV = rsi[0];
   
   // Generate trading signals
   if(emaF > emaS && rsiV < RSI_Oversold) // Buy signal: Fast EMA above Slow EMA and RSI oversold
   {
      spreadData[index].tradeAttempts++;
      double lotSize = CalculateLotSize(symbol);
      if(lotSize > 0)
      {
         if(ExecuteTrade(ORDER_TYPE_BUY, symbol, lotSize, StopLoss_Pips, TakeProfit_Pips))
         {
            spreadData[index].lastTradeTime = TimeCurrent();
            spreadData[index].successfulTrades++;
            AddDashboardMessage("BUY " + symbol + " | Spread: " + DoubleToString(spreadData[index].spreadInPips, 1));
         }
      }
   }
   else if(emaF < emaS && rsiV > RSI_Overbought) // Sell signal: Fast EMA below Slow EMA and RSI overbought
   {
      spreadData[index].tradeAttempts++;
      double lotSize = CalculateLotSize(symbol);
      if(lotSize > 0)
      {
         if(ExecuteTrade(ORDER_TYPE_SELL, symbol, lotSize, StopLoss_Pips, TakeProfit_Pips))
         {
            spreadData[index].lastTradeTime = TimeCurrent();
            spreadData[index].successfulTrades++;
            AddDashboardMessage("SELL " + symbol + " | Spread: " + DoubleToString(spreadData[index].spreadInPips, 1));
         }
      }
   }
}

ここでは、任意の時点でどの銘柄を売買候補として扱うかを決定するスプレッドの効率性ランキングエンジンを導入します。RankSymbolsBySpread()では、各銘柄に対して2つの執行品質要因、すなわち絶対スプレッドとスプレッド対ATR比に基づいた複合スプレッドスコアが割り当てられます。両方の要素はコストが低いほど高いスコアになるように反転され、その後、ボラティリティの状況も考慮しつつ、実際のスプレッドをより重視する形で重み付け統合されます。スコアが算出された後、銘柄は降順にソートされ、最もコスト効率の高い銘柄が自然に優先リストの上位に位置するようになります。

ランキング後、EAは動的な有効化フィルターを適用し、上位MaxActiveSymbolsのみを有効化し、それ以外を非アクティブとしてマークします。この仕組みにより、他の条件上は取引可能であっても、執行条件が劣る銘柄に対してリソースや資本が消費されることを防ぎます。このシステムは銘柄を恒久的に除外するのではなく、スプレッドの変動に応じて継続的に再評価および再ソートをおこないます。その結果、過去に非アクティブとなっていた銘柄も、執行品質が改善すれば再びローテーションに戻ることができます。これにより、リアルタイムのコスト効率によって駆動される自己調整型の銘柄群が実現されます。

第二の処理であるExecuteTradingLogic()は、すべてのスプレッドおよびアクティベーションフィルターを通過した銘柄に対してのみ実行され、執行品質と戦略ロジックを明確に分離します。この関数は必要なEMAおよびRSIインジケーターを取得し、データの有効性を検証した上で、トレンドの整合性とモメンタムの過熱感に基づいて売買シグナルを生成します。シグナルが確認された場合、EAは取引試行回数を記録し、ポジションサイズを計算し、取引を実行し、その後銘柄レベルのパフォーマンス指標およびダッシュボードメッセージを更新します。この構造により、取引判断は常に最もランキングの高い銘柄にのみ適用され、適応的かつ執行品質を考慮したマルチペア取引という中核思想が強化されます。

//+------------------------------------------------------------------+
//| Execute trade with CTrade                                        |
//+------------------------------------------------------------------+
bool ExecuteTrade(ENUM_ORDER_TYPE tradeType, string symbol, double lotSize, int stopLossPips, int takeProfitPips)
{
   // Get symbol info
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
   
   // Get current price
   double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
   double price = (tradeType == ORDER_TYPE_BUY) ? ask : bid;
   
   // Calculate pip size for different instruments
   double pipSize = CalculatePipSize(symbol, digits, point);
   
   // Use ATR for dynamic SL/TP if enabled
   double slDistance = 0, tpDistance = 0;
   
   if(UseATR_SL_TP)
   {
      double atr = iATR(symbol, TradingTimeframe, 14);
      if(atr > 0)
      {
         slDistance = atr * ATR_SL_Multiplier;
         tpDistance = atr * ATR_TP_Multiplier;
      }
   }
   
   // Fallback to fixed pips if ATR not used or failed
   if(slDistance == 0) slDistance = stopLossPips * pipSize;
   if(tpDistance == 0) tpDistance = takeProfitPips * pipSize;
   
   // Calculate SL and TP prices
   double sl = 0, tp = 0;
   
   if(slDistance > 0)
   {
      sl = (tradeType == ORDER_TYPE_BUY) ? price - slDistance : price + slDistance;
      sl = NormalizeDouble(sl, digits);
   }
   
   if(tpDistance > 0)
   {
      tp = (tradeType == ORDER_TYPE_BUY) ? price + tpDistance : price - tpDistance;
      tp = NormalizeDouble(tp, digits);
   }
   
   // Execute trade with CTrade
   bool success = false;
   
   if(tradeType == ORDER_TYPE_BUY)
   {
      success = Trade.Buy(lotSize, symbol, price, sl, tp, "Adaptive Spread EA");
   }
   else if(tradeType == ORDER_TYPE_SELL)
   {
      success = Trade.Sell(lotSize, symbol, price, sl, tp, "Adaptive Spread EA");
   }
   
   if(success)
   {
      PrintFormat("%s %s | Lot: %.2f | Price: %.5f | SL: %.5f | TP: %.5f | Spread: %.1f",
                  EnumToString(tradeType), symbol, lotSize, price, sl, tp, 
                  SymbolInfoDouble(symbol, SYMBOL_ASK) - SymbolInfoDouble(symbol, SYMBOL_BID));
      return true;
   }
   else
   {
      PrintFormat("Failed to open %s on %s | Error: %d", 
                  EnumToString(tradeType), symbol, GetLastError());
      return false;
   }
}

//+------------------------------------------------------------------+
//| Calculate pip size for different instruments                     |
//+------------------------------------------------------------------+
double CalculatePipSize(string symbol, int digits, double point)
{
   // Detect pip size automatically
   if(StringFind(symbol, "JPY") != -1)              // JPY pairs
      return (digits == 3) ? point * 10 : point;
   else if(StringFind(symbol, "XAU") != -1 || StringFind(symbol, "GOLD") != -1)   // Metals
      return 0.10;
   else if(StringFind(symbol, "BTC") != -1 || StringFind(symbol, "ETH") != -1)    // Cryptos
      return point * 100.0;
   else if(StringFind(symbol, "US") != -1 && digits <= 2)                         // Indices
      return point;
   else
      return (digits == 3 || digits == 5) ? point * 10 : point;                   // Default Forex
}

//+------------------------------------------------------------------+
//| Calculate position size based on risk                            |
//+------------------------------------------------------------------+
double CalculateLotSize(string symbol)
{
   double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
   double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
   
   if(accountBalance <= 0) return minLot;
   
   // Simple lot calculation based on risk percentage
   double riskAmount = accountBalance * (RiskPerTrade / 100.0);
   double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   
   if(tickValue <= 0)
   {
      // Fallback calculation
      double contractSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
      tickValue = (SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE) * contractSize) / SymbolInfoDouble(symbol, SYMBOL_POINT);
   }
   
   double pipSize = CalculatePipSize(symbol, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS), 
                                    SymbolInfoDouble(symbol, SYMBOL_POINT));
   double stopLossPoints = StopLoss_Pips * 10; // Convert pips to points
  
   if(stopLossPoints > 0 && tickValue > 0)
   {
      double lotSize = riskAmount / (stopLossPoints * tickValue);
      lotSize = NormalizeDouble(lotSize, 2);
      
      // Apply lot size limits
      lotSize = MathMax(lotSize, minLot);
      lotSize = MathMin(lotSize, maxLot);
      lotSize = MathRound(lotSize / lotStep) * lotStep;
      
      return lotSize;
   }
   
   return minLot;
}

本セクションでは、EAの取引実行およびリスク管理レイヤーを定義します。まずExecuteTrade()から始まり、この関数は銘柄の種類に依存せず、注文の発注方法を標準化します。この関数は最初に銘柄固有の価格情報を取得し、取引方向に応じて適切な約定価格を決定します。その後、FXペア、貴金属、指数、暗号資産といった異なるアセットクラスに対してハードコードされた前提を持たずに対応できるよう、ピップサイズを動的に計算します。ストップロスおよびテイクプロフィットの距離は、ATRベースのボラティリティ計測、またはそれが利用できない場合のフォールバックとしての固定ピップ値のいずれかから導出されます。これにより、さまざまな市場環境においても堅牢性が確保されます。価格レベルが銘柄の精度に正規化された後、CTradeクラスを用いて取引が実行されます。成功および失敗の両方について詳細なログを出力することで、透明性とデバッグ性が維持されます。

補助関数であるCalculatePipSize()およびCalculateLotSize()は、異種の金融商品間において一貫したポジションサイズ管理とリスク制御を実現します。CalculatePipSize()は銘柄特性に基づいてピップ定義を自動的に適応させることで、異なるアセットクラスを正確に取引できるようにします。CalculateLotSize()はアカウント残高とリスク割合からポジションサイズを計算し、金額ベースのリスクを取引数量へと変換します。その際、ブローカーの制約である最小ロット、最大ロット、ステップサイズも考慮します。これらの関数により、すべての取引は制御されたリスクのもとで、銘柄特性を考慮した精度と一貫性を持って実行されます。これにより、EAの適応的なマルチ銘柄実行フレームワークがさらに強化されます。

void UpdateDashboard()
{
   if(!ShowDashboard) return;
   
   // Update ranking
   for(int i = 0; i < MathMin(MaxActiveSymbols + 2, TotalPairs); i++)
   {
      string objName = "Dashboard_Rank_" + IntegerToString(i);
      string status = spreadData[i].isActive ? "Yes" : "No";
      
      string text = IntegerToString(i+1) + ". " + spreadData[i].symbol + 
                    " | Spread: " + DoubleToString(spreadData[i].spreadInPips, 1) + 
                    " | Score: " + DoubleToString(spreadData[i].spreadScore, 3) + 
                    " | Active: " + status;
      
      ObjectSetString(0, objName, OBJPROP_TEXT, text);
      ObjectSetInteger(0, objName, OBJPROP_COLOR, spreadData[i].statusColor);
   }
   
   // Update messages
   for(int i = 0; i < 10; i++)
   {
      string objName = "Dashboard_Msg_" + IntegerToString(i);
      ObjectSetString(0, objName, OBJPROP_TEXT, DashboardMessages[i]);
   }
}

void AddDashboardMessage(string message)
{
   // Shift messages up
   for(int i = 9; i > 0; i--)
   {
      DashboardMessages[i] = DashboardMessages[i-1];
   }
   
   // Add new message at the beginning
   DashboardMessages[0] = TimeToString(TimeCurrent(), TIME_SECONDS) + ": " + message;
}

//+------------------------------------------------------------------+
//| Chart Event Handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
{
   if(ShowDashboard && (id == CHARTEVENT_CHART_CHANGE || id == CHARTEVENT_CLICK))
   {
      UpdateDashboard();
   }
}

//+------------------------------------------------------------------+
//| Dashboard Functions                                              |
//+------------------------------------------------------------------+
void CreateDashboard()
{
   // Create main dashboard background
   ObjectCreate(0, "Dashboard_BG", OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_XDISTANCE, DashboardX);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_YDISTANCE, DashboardY);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_XSIZE, 400);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_YSIZE, 350);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_BGCOLOR, DashboardBGColor);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_BORDER_COLOR, clrGray);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, "Dashboard_BG", OBJPROP_HIDDEN, true);
   
   // Create title
   ObjectCreate(0, "Dashboard_Title", OBJ_LABEL, 0, 0, 0);
   ObjectSetString(0, "Dashboard_Title", OBJPROP_TEXT, "=== Adaptive Spread EA ===");
   ObjectSetInteger(0, "Dashboard_Title", OBJPROP_XDISTANCE, DashboardX + 10);
   ObjectSetInteger(0, "Dashboard_Title", OBJPROP_YDISTANCE, DashboardY + 10);
   ObjectSetInteger(0, "Dashboard_Title", OBJPROP_COLOR, clrYellow);
   ObjectSetInteger(0, "Dashboard_Title", OBJPROP_FONTSIZE, FontSize + 2);
   ObjectSetString(0, "Dashboard_Title", OBJPROP_FONT, "Consolas");
   ObjectSetInteger(0, "Dashboard_Title", OBJPROP_CORNER, CORNER_LEFT_UPPER);
   
   // Create ranking header
   ObjectCreate(0, "Dashboard_RankHeader", OBJ_LABEL, 0, 0, 0);
   ObjectSetString(0, "Dashboard_RankHeader", OBJPROP_TEXT, "=== Symbol Ranking ===");
   ObjectSetInteger(0, "Dashboard_RankHeader", OBJPROP_XDISTANCE, DashboardX + 10);
   ObjectSetInteger(0, "Dashboard_RankHeader", OBJPROP_YDISTANCE, DashboardY + 35);
   ObjectSetInteger(0, "Dashboard_RankHeader", OBJPROP_COLOR, clrYellow);
   ObjectSetInteger(0, "Dashboard_RankHeader", OBJPROP_FONTSIZE, FontSize);
   ObjectSetString(0, "Dashboard_RankHeader", OBJPROP_FONT, "Consolas");
   ObjectSetInteger(0, "Dashboard_RankHeader", OBJPROP_CORNER, CORNER_LEFT_UPPER);
   
   // Create symbol ranking labels
   for(int i = 0; i < MaxActiveSymbols + 2; i++)
   {
      string objName = "Dashboard_Rank_" + IntegerToString(i);
      ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, DashboardX + 10);
      ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, DashboardY + 55 + (i * 20));
      ObjectSetInteger(0, objName, OBJPROP_COLOR, DashboardTextColor);
      ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, FontSize);
      ObjectSetString(0, objName, OBJPROP_FONT, "Consolas");
      ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   }
   
   // Create messages header
   ObjectCreate(0, "Dashboard_MsgHeader", OBJ_LABEL, 0, 0, 0);
   ObjectSetString(0, "Dashboard_MsgHeader", OBJPROP_TEXT, "=== Messages ===");
   ObjectSetInteger(0, "Dashboard_MsgHeader", OBJPROP_XDISTANCE, DashboardX + 10);
   ObjectSetInteger(0, "Dashboard_MsgHeader", OBJPROP_YDISTANCE, DashboardY + 180);
   ObjectSetInteger(0, "Dashboard_MsgHeader", OBJPROP_COLOR, clrYellow);
   ObjectSetInteger(0, "Dashboard_MsgHeader", OBJPROP_FONTSIZE, FontSize);
   ObjectSetString(0, "Dashboard_MsgHeader", OBJPROP_FONT, "Consolas");
   ObjectSetInteger(0, "Dashboard_MsgHeader", OBJPROP_CORNER, CORNER_LEFT_UPPER);
   
   // Create message labels
   for(int i = 0; i < 10; i++)
   {
      string objName = "Dashboard_Msg_" + IntegerToString(i);
      ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, DashboardX + 10);
      ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, DashboardY + 200 + (i * 15));
      ObjectSetInteger(0, objName, OBJPROP_COLOR, DashboardTextColor);
      ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, FontSize);
      ObjectSetString(0, objName, OBJPROP_FONT, "Consolas");
      ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   }
}

void RemoveDashboard()
{
   ObjectsDeleteAll(0, "Dashboard_");
}

このコードは、EAの内部判断プロセスをチャート上に直接可視化するリアルタイムのビジュアルダッシュボードレイヤーを実装しています。UpdateDashboard()関数は、上位ランクの銘柄に対してスプレッド値、スプレッドの効率性スコア、アクティブ状態を表示することで銘柄ランキングを更新し、各銘柄の現在の取引可能状態を色によって視覚的に表現します。ランキングデータに加えて、ダッシュボードはロール形式のメッセージログも表示します。このログは、銘柄の無効化や取引実行など、システムの重要なイベントを記録します。AddDashboardMessage()ヘルパー関数は、古いメッセージを下にシフトし、新しいエントリにタイムスタンプを付与することでこのログを管理し、常に最新かつ重要な情報が一目で確認できるようにします。

残りの関数はダッシュボードのライフサイクルおよびインタラクションを処理します。OnChartEvent()は、チャートの更新やユーザー操作に応じてダッシュボードを強制的に更新することで、表示内容を常に同期された状態に保ちます。CreateDashboard()は、ダッシュボード全体のインターフェースをゼロから構築します。背景パネル、セクションヘッダー、銘柄ランキング表示、メッセージ行などを含み、それぞれが明確さとチャートへの干渉を最小限に抑える配置およびスタイルで設計されています。最後にRemoveDashboard()は、EAが削除された際にダッシュボード関連オブジェクトをすべて削除することでクリーンな終了処理を提供し、視覚的な残留物を残さず、チャート環境を整理された状態に保ちます。


バックテスト結果

テストは2025年11月19日から2026年1月17日までのおよそ2か月間にわたり実施され、以下の設定で行われました。

入力設定

次に、エクイティカーブおよびバックテスト結果を示します。

エクイティカーブ

バックテスト結果


結論

本システムでは動的マルチペアEA内における執行判断レイヤーとして機能する「適応型スプレッド感度フレームワーク」を設計し、実装しました。本システムはリアルタイムのスプレッドを継続的に監視し、ボラティリティの状況で正規化した上でスコアリングをおこない、コスト効率に基づいて銘柄をランキングし、現在の執行品質に応じて動的に有効化と無効化をおこないます。スプレッド評価と取引ロジックを分離することで、銘柄選択、優先順位付け、および保護メカニズムが適応的かつ軽量でスケーラブルな構造となり、基盤となる戦略ルールを変更することなく、高頻度で銘柄間のフォーカスを切り替えることが可能になります。

結論として、このアプローチはトレーダーに対し、急速に変化するマルチ銘柄環境において取引コストと執行リスクを制御する強力な手段を提供します。すべてのペアを無差別に取引するのではなく、その時点で最も効率の良い市場に取引を集中させることで、スリッページの削減、不利なスプレッド条件の回避、そして全体的な取引品質の向上を実現します。その結果、トレーダーはよりクリーンな約定、より高い資本効率、そして静的な前提に制約されることなく市場マイクロストラクチャの変化に適応する、より堅牢な自動売買システムを得ることができます。

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

添付されたファイル |
初心者からエキスパートへ:サプライ&デマンドゾーンの統計的検証 初心者からエキスパートへ:サプライ&デマンドゾーンの統計的検証
サプライ&デマンド取引戦略の背後にある、見落とされがちな統計的基盤を明らかにします。MQL5とPythonをJupyter Notebookワークフローで連携させることで、マーケットに対する視覚的な仮定を定量的な洞察へと変換する体系的なデータ駆動型リサーチをおこないます。本記事では、データ収集からPythonによる統計分析、アルゴリズム設計、テスト、最終的な結論に至るまで、一連の研究プロセスを解説します。手法と結果の詳細については、本文をご参照ください。
MQL5における取引戦略の自動化(第47回):ヘッジ機能を備えたNick Rypock Trailing Reverse (NRTR) MQL5における取引戦略の自動化(第47回):ヘッジ機能を備えたNick Rypock Trailing Reverse (NRTR)
MQL5でNick Rypock Trailing Reverse (NRTR)取引システムを開発します。このシステムは、NRTRチャネルインジケータを用いて反転シグナルを検出し、トレンドフォロー型のエントリーを実現します。また、買いポジションと売りポジションの両方に対応したヘッジ機能も備えています。さらに、エクイティまたは口座残高に基づく自動ロット計算、ATR倍率を用いた固定または動的なストップロスおよびテイクプロフィット設定、ならびにポジション数制限などのリスク管理機能も実装します。
ラリー・ウィリアムズ/ja/『市場/ja/秘密』(第9回):利益につながるパターン ラリー・ウィリアムズ/ja/『市場/ja/秘密』(第9回):利益につながるパターン
ラリー・ウィリアムズ/ja/短期取引パターンに関する実証研究です。定番/ja/パターンをMQL5で自動化し、実際/ja/市場データでテストし、そ/ja/一貫性、収益性、および実運用上/ja/有用性を評価します。
MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張 MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張
MQL5で多用途なRSIインジケータを構築します。このインジケータは複数のバリエーション、データソース、平滑化手法をサポートし、より高度な分析を可能にします。さらに、視覚的な色表現のための色相シフト、買われすぎ・売られすぎゾーンの動的境界、トレンドアラート用の通知機能を追加します。また、補間を伴うマルチタイムフレーム対応も実装し、異なる時間足のRSI値を補間によって滑らかに対応付けるカスタマイズ可能なRSIツールを提供します。