ダイナミックマルチペアEAの形成(第6回):高頻度銘柄切り替えのための適応型スプレッド感度制御
はじめに
本連載第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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初心者からエキスパートへ:サプライ&デマンドゾーンの統計的検証
MQL5における取引戦略の自動化(第47回):ヘッジ機能を備えたNick Rypock Trailing Reverse (NRTR)
ラリー・ウィリアムズ/ja/『市場/ja/秘密』(第9回):利益につながるパターン
MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索