English
preview
Разработка динамического мультивалютного советника (Часть 6): Адаптивная чувствительность к спреду при высокочастотном переключении символов

Разработка динамического мультивалютного советника (Часть 6): Адаптивная чувствительность к спреду при высокочастотном переключении символов

MetaTrader 5Примеры |
53 0
Hlomohang John Borotho
Hlomohang John Borotho
Содержание:
  1. Введение
  2. Обзор системы и стратегический подход
  3. Приступим
  4. Результаты тестирования на исторических данных
  5. Заключение


Введение

Часть 5 нашей серии о динамическом мультивалютном советнике была посвящена выбору подходящего стиля торговли для разных рыночных условий, в частности построению системы, способной переключаться между режимами скальпинга и свинг-трейдинга. Мы подробно разобрали, как скальпинг ориентирован на захват небольших ценовых движений на коротких таймфреймах, тогда как свинг-трейдинг нацелен на более крупные направленные движения в течение более длительных периодов, и показали, как советник может динамически адаптировать свою логику, уровни стопов и временные горизонты в зависимости от рыночного контекста.

В отличие от этого, часть 6 смещает акцент с конкретных стилей входа и выхода из сделок на саму среду исполнения, прежде всего на торговые издержки, выраженные через спреды. В части 6 мы вводим модуль, который непрерывно отслеживает текущие условия спреда по всем символам и адаптируется к ним, используя динамические пороги чувствительности, чтобы определять символы, оптимальные для торговли в данный момент. Такое высокочастотное переключение символов на основе адаптивной оценки спреда дополняет общую мультивалютную архитектуру: приоритет отдается качеству исполнения и эффективности издержек, а не только механике стратегии.


Обзор системы и стратегический подход

Советник Adaptive Spread Sensitivity представляет собой продвинутую мультивалютную торговую систему, предназначенную для динамической оптимизации исполнения сделок по нескольким финансовым инструментам. В основе системы лежит непрерывный мониторинг спредов в реальном времени по всем настроенным символам. Затем система ранжирует их по метрикам эффективности издержек, отдавая приоритет инструментам с наиболее выгодными условиями исполнения.

Symbol Ranking Mechanism

В отличие от традиционных односимвольных советников, эта система реализует интеллектуальную фильтрацию спреда, которая временно исключает из торговли символы с аномально высоким спредом, предотвращая входы с повышенными издержками в условиях низкой ликвидности, и автоматически возвращает их, когда спред нормализуется. Адаптивная архитектура позволяет советнику работать как "умный маршрутизатор", динамически переключаясь между доступными символами с учетом меняющейся микроструктуры рынка. Благодаря этому сделки в каждый момент исполняются по инструменту с наилучшими условиями по издержкам.

Spread Protection Mechanism

Торговая стратегия использует простой, но эффективный подход технического анализа: пересечения двух скользящих средних в сочетании с подтверждением импульса по RSI. Когда условия по спреду благоприятны, система формирует сигналы на покупку, если быстрая EMA поднимается выше медленной, а RSI указывает на перепроданность; сигналы на продажу – если быстрая EMA пересекает медленную сверху вниз, а RSI находится в зоне перекупленности. Это сочетание обеспечивает более сбалансированный момент входа: скользящие средние задают направление тренда, а RSI повышает точность входа.

Real-Time Dash View


Приступим

//+------------------------------------------------------------------+
//|                                           Spread Sensitivity.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/ru/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/ru/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];

Для начала мы формируем гибкий слой конфигурации для нашего динамического мультивалютного советника, сгруппировав все пользовательские входные параметры в четкой модульной структуре. Сначала задаются общие торговые параметры: список отслеживаемых символов, риск на сделку и магическое число для отслеживания позиций. Далее создается специальный раздел Spread Sensitivity – ключевой модуль исполнения советника. Эти входные параметры задают абсолютные и адаптивные лимиты спреда, пороги спреда, нормализованные по ATR, временное отключение символов и ограничения ранжирования, позволяя советнику решать, какими рынками сейчас целесообразно торговать с точки зрения издержек. Важно, что этот слой определяет не способ входа в сделку, а лишь то, может ли символ участвовать в торговле, тем самым обеспечивая качество исполнения по нескольким инструментам.

Далее в коде задаются входные параметры уровня стратегии и вспомогательная инфраструктура, которые задействуются только после того, как символ пройдет фильтр спреда. Торговые настройки включают параметры индикаторов (EMA, RSI), границы риска (SL/TP, таймауты, максимальное число позиций) и опциональные динамические выходы на основе ATR, что обеспечивает контролируемое и последовательное исполнение сделок. Ниже блока входных параметров глобальные переменные и структура SpreadData образуют внутренний механизм управления состоянием советника, отслеживая метрики спреда в реальном времени, статусы символов, флаги активности, торговую статистику и элементы информационной панели. Эта структура позволяет советнику ранжировать символы, динамически отключать и повторно включать их, а также отображать текущее состояние системы на информационной панели, благодаря чему логика советника остается адаптивной, а его работа – прозрачной.

//+------------------------------------------------------------------+
//| 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() выполняет полную подготовку советника к запуску, настраивая символы, внутренние структуры данных и параметры исполнения до начала торговли. Сначала выполняется разбор заданного пользователем списка символов и проверяется, доступен ли хотя бы один торгуемый символ; если нет, инициализация корректно завершается. Затем для каждого символа выделяется и инициализируется структура spreadData: задаются значения по умолчанию для метрик спреда, торгового состояния, статистики и визуальных индикаторов статуса, а сам символ подписывается на обновления данных в реальном времени. Наконец, настраиваются параметры исполнения сделок, такие как магическое число и допустимое проскальзывание, при необходимости инициализируется информационная панель на графике, в лог записывается сообщение о запуске и подтверждается успешная инициализация. Это обеспечивает полную синхронизацию советника, его мониторинг и готовность к адаптивной работе с несколькими символами.

//+------------------------------------------------------------------+
//| 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() обеспечивает корректное и информативное завершение работы советника. Когда советник удаляется или терминал закрывается, функция сначала удаляет с графика все объекты информационной панели, чтобы не оставалось лишних визуальных элементов. Затем в лог выводится краткая сводка по торговым результатам для каждого символа: число попыток открытия сделок и число успешных исполнений, зафиксированных во время работы. Этот заключительный этап логирования повышает прозрачность и упрощает диагностику после завершения работы, позволяя оценить поведение отдельных символов и эффективность системы до полной деинициализации советника.

Функция OnTick(), в свою очередь, задает рабочий цикл советника в реальном времени и построена с расчетом на эффективность в многосимвольной среде. Вместо обработки всех символов на каждом тике используется циклический проход по одному символу за тик с помощью счетчика по модулю, что снижает нагрузку на процессор и позволяет избежать узких мест при выполнении. Для каждого выбранного символа обновляются данные спреда, проверяется допуск к торговле, соблюдаются ограничения по таймауту, после чего при выполнении всех условий запускается торговая логика. Обновление информационной панели выполняется периодически, а не на каждом тике, благодаря чему интерфейс остается отзывчивым, а производительность – стабильной даже при высокочастотном мониторинге символов.

//+------------------------------------------------------------------+
//| 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() служит планировщиком: периодически обновляет данные по спреду для всех настроенных символов и, если эта опция включена, ранжирует их по эффективности спреда. Такая схема отделяет анализ спреда от ценовых тиков, поэтому советник остается отзывчивым даже в периоды низкой ликвидности. Цепочка обновления проходит через 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() каждому символу присваивается составной балл спреда на основе двух факторов качества исполнения: абсолютного спреда и отношения спреда к ATR. Оба компонента инвертируются так, что меньшие издержки дают более высокий балл, а затем объединяются с учетом весов, чтобы подчеркнуть абсолютный спред, но сохранить и контекст волатильности. После расчета баллов символы сортируются по убыванию, что гарантирует, что наиболее экономичные инструменты будут естественным образом подниматься в верхнюю часть списка приоритетов.

После ранжирования советник применяет динамический фильтр активации: оставляет активными только MaxActiveSymbols лучших символов, а остальные помечает как неактивные. Этот механизм гарантирует, что советник не будет тратить ресурсы и капитал на символы с худшими условиями исполнения, даже если по остальным критериям они допустимы. Вместо постоянного исключения символов система непрерывно переоценивает их и перестраивает порядок по мере изменения спредов, позволяя ранее неактивным символам возвращаться в ротацию, когда качество исполнения улучшается. Так формируется адаптивный набор инструментов, автоматически балансирующийся по качеству исполнения и полностью управляемый текущей эффективностью издержек.

Вторая часть, ExecuteTradingLogic(), выполняется только для символов, прошедших все фильтры по спреду и активации, четко отделяя качество исполнения от логики стратегии. Она получает необходимые значения индикаторов EMA и RSI, проверяет доступность данных, а затем формирует торговые сигналы на основе согласованности тренда и истощения импульса. Когда сигнал подтвержден, советник фиксирует попытку сделки, рассчитывает размер позиции, исполняет сделку и обновляет метрики эффективности по символу и сообщения на информационной панели. Такая структура гарантирует, что торговые решения будут применяться только к символам с наивысшим рангом, усиливая базовую идею адаптивной мультивалютной торговли с учетом качества исполнения.

//+------------------------------------------------------------------+
//| 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;
}

В этом разделе описывается слой исполнения сделок и контроля риска в советнике; отправной точкой служит ExecuteTrade(), который унифицирует открытие ордеров независимо от типа символа. Сначала функция получает ценовые параметры конкретного символа и определяет корректную цену исполнения в зависимости от направления сделки. Затем она динамически рассчитывает размер пипса, чтобы без жестко заданных допущений универсально поддерживать валютные пары, металлы, индексы и криптовалюты. Расстояния для стоп-лосса и тейк-профита определяются либо на основе волатильности по ATR, либо, как резервный вариант, по фиксированным значениям в пипсах, что обеспечивает устойчивую работу в разных рыночных условиях. После нормализации ценовых уровней с учетом точности символа сделки исполняются через класс CTrade, а подробное логирование успешных и неудачных операций сохраняет прозрачность и упрощает отладку.

Вспомогательные функции CalculatePipSize() и CalculateLotSize() обеспечивают единообразный расчет размера позиции и управление риском для разнородных инструментов. CalculatePipSize() автоматически подстраивает размер пипса под характеристики символа, позволяя советнику корректно работать со смешанными классами активов. Затем CalculateLotSize() вычисляет размер позиции на основе баланса счета и процента риска, переводя денежный риск в объем с учетом ограничений брокера – минимального, максимального и шагового размера. Вместе эти функции обеспечивают исполнение каждой сделки с контролируемым риском, точностью с учетом типа инструмента и последовательным поведением, усиливая адаптивную мультисимвольную схему исполнения советника.

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_");
}

Этот код реализует визуальный слой информационной панели в реальном времени, выводящий на график внутреннюю логику принятия решений советника. Функция UpdateDashboard() обновляет ранжирование символов: выводит значения спреда, баллы эффективности спреда и статус активности для символов с наивысшим рангом, используя цветовую индикацию для отражения их текущей пригодности к торговле. Наряду с данными ранжирования панель также показывает обновляемый журнал сообщений, в котором фиксируются ключевые события системы, такие как отключение символов или исполнение сделок. Вспомогательная функция AddDashboardMessage() поддерживает этот журнал: сдвигает старые сообщения вниз и добавляет новым метку времени, так что самая свежая и актуальная информация всегда видна с первого взгляда.

Оставшиеся функции отвечают за жизненный цикл и интерактивность информационной панели. OnChartEvent() поддерживает синхронизацию панели с обновлениями графика и действиями пользователя, принудительно обновляя ее при изменении графика или клике по нему. CreateDashboard() строит весь интерфейс с нуля: фоновую панель, заголовки разделов, метки ранжирования символов и строки сообщений – все размещено и оформлено так, чтобы оставаться наглядным и минимально перекрывать график. Наконец, RemoveDashboard() выполняет аккуратное удаление: при снятии советника удаляются все объекты информационной панели, поэтому визуальных артефактов не останется, а график сохранит чистый и профессиональный вид.


Результаты тестирования на исторических данных

Тестирование проводилось в течение примерно двух месяцев – с 19 ноября 2025 года по 17 января 2026 года – при следующих настройках:

Input Settings

Теперь рассмотрим кривую капитала и результаты тестирования на исторических данных:

Eq Curve

BT Results


Заключение

Подводя итог, мы разработали и реализовали систему Adaptive Spread Sensitivity, которая работает как слой интеллектуального управления исполнением в динамическом мультивалютном советнике. Система непрерывно отслеживает текущие спреды, нормализует их с учетом волатильности, ранжирует символы по эффективности издержек и динамически активирует или деактивирует инструменты в зависимости от текущего качества исполнения. Отделив оценку спреда от торговой логики, мы сделали механизмы выбора символов, приоритизации и защиты адаптивными, легкими и масштабируемыми – это позволяет советнику часто переключать фокус между символами, не меняя базовые правила стратегии.

В итоге такой подход дает трейдерам мощный инструмент для контроля торговых издержек и риска исполнения в быстро меняющейся мультисимвольной среде. Вместо того чтобы вслепую торговать всеми парами, советник сосредоточивает торговую активность на наиболее эффективных рынках в каждый конкретный момент, что снижает проскальзывание, помогает избегать неблагоприятных спредов и повышает общее качество сделок. Для трейдеров это означает более качественное исполнение, более эффективное использование капитала и более устойчивую автоматизированную систему, которая адаптируется к меняющейся микроструктуре рынка, а не опирается на статические предпосылки.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20371

Прикрепленные файлы |
Разработка инструментария для анализа Price Action (Часть 54): Фильтрация трендов с помощью EMA и сглаженных ценовых данных Разработка инструментария для анализа Price Action (Часть 54): Фильтрация трендов с помощью EMA и сглаженных ценовых данных
В этой статье рассматривается метод, сочетающий сглаживание Heikin-Ashi с границами EMA20 по максимумам и минимумам, а также фильтром тренда EMA50, чтобы сделать сигналы понятнее, а входы точнее. Статья показывает, как эти инструменты помогают трейдерам выявлять реальный импульс, отсекать шум и увереннее работать на волатильном или трендовом рынке.
Разработка инструментария для анализа Price Action (Часть 53): Тепловая карта плотности паттернов для выявления зон поддержки и сопротивления Разработка инструментария для анализа Price Action (Часть 53): Тепловая карта плотности паттернов для выявления зон поддержки и сопротивления
В этой статье представлен советник Pattern Density Heatmap – инструмент картирования ценовой динамики, который преобразует повторяющиеся обнаружения свечных паттернов в статистически значимые зоны поддержки и сопротивления. Вместо того чтобы рассматривать каждый сигнал по отдельности, советник агрегирует обнаружения в фиксированные ценовые зоны (бины), оценивает их плотность с опциональным взвешиванием по давности и подтверждает уровни по данным старшего таймфрейма. Полученная тепловая карта показывает уровни, на которые рынок исторически реагировал, и помогает заранее выбирать момент входа, управлять риском и повышать уверенность в стратегии независимо от стиля торговли.
Переосмысливаем классические стратегии (Часть 17): Моделирование технических индикаторов Переосмысливаем классические стратегии (Часть 17): Моделирование технических индикаторов
В этом обсуждении мы сосредоточимся на том, как можно преодолеть "стеклянный потолок", создаваемый классическими методами машинного обучения в сфере финансов. Похоже, что самое главное ограничение ценности, которую можно извлечь из статистических моделей, заключается не в самих моделях — ни в данных, ни в сложности алгоритмов, — а скорее в методологии, которую мы используем для их применения. Другими словами, истинным узким местом может быть то, как мы используем модель, а не ее собственный потенциал.
Категориальная скрытая марковская модель на языке MQL5 Категориальная скрытая марковская модель на языке MQL5
В статье подробно рассматриваются теоретические основы и практическая реализация скрытой марковской модели с категориальными эмиссиями (Categorical HMM) на языке MQL5. На конкретных примерах демонстрируются процессы инференса, итерационного обучения параметров, онлайн-фильтрации, а также методология выбора оптимальной архитектуры модели по информационным критериям AIC/BIC.