Разработка динамического мультивалютного советника (Часть 6): Адаптивная чувствительность к спреду при высокочастотном переключении символов
- Введение
- Обзор системы и стратегический подход
- Приступим
- Результаты тестирования на исторических данных
- Заключение
Введение
Часть 5 нашей серии о динамическом мультивалютном советнике была посвящена выбору подходящего стиля торговли для разных рыночных условий, в частности построению системы, способной переключаться между режимами скальпинга и свинг-трейдинга. Мы подробно разобрали, как скальпинг ориентирован на захват небольших ценовых движений на коротких таймфреймах, тогда как свинг-трейдинг нацелен на более крупные направленные движения в течение более длительных периодов, и показали, как советник может динамически адаптировать свою логику, уровни стопов и временные горизонты в зависимости от рыночного контекста.
В отличие от этого, часть 6 смещает акцент с конкретных стилей входа и выхода из сделок на саму среду исполнения, прежде всего на торговые издержки, выраженные через спреды. В части 6 мы вводим модуль, который непрерывно отслеживает текущие условия спреда по всем символам и адаптируется к ним, используя динамические пороги чувствительности, чтобы определять символы, оптимальные для торговли в данный момент. Такое высокочастотное переключение символов на основе адаптивной оценки спреда дополняет общую мультивалютную архитектуру: приоритет отдается качеству исполнения и эффективности издержек, а не только механике стратегии.
Обзор системы и стратегический подход
Советник Adaptive Spread Sensitivity представляет собой продвинутую мультивалютную торговую систему, предназначенную для динамической оптимизации исполнения сделок по нескольким финансовым инструментам. В основе системы лежит непрерывный мониторинг спредов в реальном времени по всем настроенным символам. Затем система ранжирует их по метрикам эффективности издержек, отдавая приоритет инструментам с наиболее выгодными условиями исполнения.

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

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

Приступим
//+------------------------------------------------------------------+ //| 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 года – при следующих настройках:

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


Заключение
Подводя итог, мы разработали и реализовали систему Adaptive Spread Sensitivity, которая работает как слой интеллектуального управления исполнением в динамическом мультивалютном советнике. Система непрерывно отслеживает текущие спреды, нормализует их с учетом волатильности, ранжирует символы по эффективности издержек и динамически активирует или деактивирует инструменты в зависимости от текущего качества исполнения. Отделив оценку спреда от торговой логики, мы сделали механизмы выбора символов, приоритизации и защиты адаптивными, легкими и масштабируемыми – это позволяет советнику часто переключать фокус между символами, не меняя базовые правила стратегии.
В итоге такой подход дает трейдерам мощный инструмент для контроля торговых издержек и риска исполнения в быстро меняющейся мультисимвольной среде. Вместо того чтобы вслепую торговать всеми парами, советник сосредоточивает торговую активность на наиболее эффективных рынках в каждый конкретный момент, что снижает проскальзывание, помогает избегать неблагоприятных спредов и повышает общее качество сделок. Для трейдеров это означает более качественное исполнение, более эффективное использование капитала и более устойчивую автоматизированную систему, которая адаптируется к меняющейся микроструктуре рынка, а не опирается на статические предпосылки.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20371
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария для анализа Price Action (Часть 54): Фильтрация трендов с помощью EMA и сглаженных ценовых данных
Разработка инструментария для анализа Price Action (Часть 53): Тепловая карта плотности паттернов для выявления зон поддержки и сопротивления
Переосмысливаем классические стратегии (Часть 17): Моделирование технических индикаторов
Категориальная скрытая марковская модель на языке MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования