Разработка динамического мультивалютного советника (Часть 9): Фильтрация шума исполнения с учетом микроструктуры рынка
Содержание
Введение
Большинство трейдеров строят свои стратегии на основе ценового движения, индикаторов или статистических преимуществ. Затем они удивляются, почему система, которая показывает отличные результаты на бэктесте, теряет деньги в реальной торговле. Виновником редко бывает сигнал. Дело в моменте исполнения. Спреды могут внезапно расширяться во время публикации новостей, переноса позиций и торговых сессий с низкой ликвидностью. Поток тиков становится нестабильным, когда алгоритмы институциональных инвесторов меняют позиции. Цена скачком проходит уровни, которые должны были служить поддержкой. Проскальзывание при исполнении обычного рыночного ордера съедает половину ожидаемой прибыли. Трейдер наблюдает, как его преимущество тает на глазах — не потому, что направление было выбрано неверно, а потому, что микроструктура рынка находилась в состоянии контролируемого хаоса именно в тот момент, когда система попыталась совершить сделку. Ничего из этого не видно на стандартном свечном графике. Большинство экспертных советников (EA) для розничных трейдеров не имеют механизмов обнаружения таких явлений и реагирования на них.
Решение заключается в том, чтобы рассматривать качество исполнения как фильтр первостепенной важности. Он выступает в качестве промежуточного слоя между логикой вашей стратегии и рынком. Он разрешает торговлю только тогда, когда технические условия исполнения благоприятны. Вместо того чтобы открывать сделку по каждому достоверному сигналу, советник сначала проверяет условия исполнения. Он проверяет спред, скорость тиков, недавние разрывы в котировках и стабильность микроволатильности. Только после успешного прохождения всех проверок система приступает к оценке самого торгового сигнала. Этот подход сочетается со стратегией продолжения движения после свипа ликвидности, нацеленной на использование последствий охоты за стопами. В результате получается советник, который не просто разумнее выбирает моменты для торговли — сама его архитектура исключает торговлю в условиях, на которые фактически приходится большая часть убытков розничных трейдеров.
Обзор системы
Этот советник (EA) работает скорее как многоуровневый фильтр исполнения, чем как обычный генератор сигналов. Он независимо отслеживает несколько торговых инструментов, поддерживая для каждого из них буфер тиков, историю спреда и профиль волатильности. Прежде чем будет открыта любая сделка, необходимо пройти пять фильтров шума: расширения спреда, скорости тиков, разрывов в котировках, микроволатильности и стабильности исполнения. Только после этого активируется уровень торговой стратегии. Сама торговая логика использует модель продолжения движения после свипа ликвидности. Советник дожидается завершения охоты за стопами, подтверждает структурный сдвиг и открывает позицию в направлении нового потока ордеров. Система управления рисками ограничивает совокупную рыночную экспозицию и количество коррелированных позиций, а также корректирует размеры лотов с учётом средств счёта.

Советник не прогнозирует направление движения. Он прогнозирует, позволяет ли текущее состояние рынка исполнить ордер без утраты торгового преимущества.
Обычные советники рассматривают каждый допустимый сигнал как команду к открытию сделки. Этот советник использует сигнал лишь при определённом условии: текущее состояние рынка должно позволять качественно исполнить ордер. Он не прогнозирует направление движения точнее. Это позволяет избегать моментов, когда направление движения не имеет значения, поскольку качество исполнения слишком низкое, чтобы извлечь из этого движения прибыль. Стратегия нацелена на свипы ликвидности, поскольку при этих событиях концентрируются стоп-лоссы розничных трейдеров и возникают резкие кратковременные рыночные искажения. Ожидая затухания шума после свипа ликвидности, советник входит в рынок, когда ложное движение уже завершилось и возобновился реальный поток ордеров.

Советник не гонится за свипом ликвидности. Он дожидается, пока хаос уляжется, убеждается, что рынок определил направление движения, и только после этого вкладывает капитал.
Начало работы
//+------------------------------------------------------------------+ //| Market Micro.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/en/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/users/johnhlomohang/" #property version "1.00" #property description "Market Microstructure Execution Noise Filtering EA" #include <Trade\Trade.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\OrderInfo.mqh> #include <Trade\SymbolInfo.mqh> //+------------------------------------------------------------------+ //| INPUT PARAMETERS | //+------------------------------------------------------------------+ input group "═══ Symbol Configuration ═══" input string Symbols = "XAUUSD,GBPUSD,USDJPY,USDZAR"; input group "═══ Noise Filter Engine ═══" input bool EnableNoiseFilter = true; input double MaxSpreadMultiplier = 2.0; input double MaxTickVelocity = 40.0; input double MaxGapPoints = 150.0; input double MinVolatilityPoints = 3.0; input double MaxVolatilityPoints = 120.0; input int SpreadSamplePeriod = 200; input int VolatilitySampleTicks = 20; input double SlippageStabilityFactor = 2.5; input group "═══ Liquidity Sweep Strategy ═══" input int SwingLookback = 20; input int StructureConfirmBars = 3; input double SweepTolerancePoints = 10.0; input int NoiseSettleSeconds = 30; input bool EnableBuySignals = true; input bool EnableSellSignals = true; input group "═══ Risk Management ═══" input double RiskPercent = 1.0; input double MaxTotalExposurePercent = 5.0; input int MaxTotalPositions = 4; input double MaxCorrelatedRisk = 3.0; input double MaxDrawdownPercent = 10.0; input double StopLossATRMultiplier = 1.5; input double TakeProfitRR = 2.0; input int ATRPeriod = 14; input group "═══ Execution Engine ═══" input int MaxSlippagePoints = 30; input int MaxRetries = 3; input int RetryDelayMS = 200; input ulong MagicNumber = 20250101; input group "═══ Logging & Diagnostics ═══" input bool EnableDetailedLog = true; input bool EnableTradeLog = true; input bool EnableScoreDisplay = true; input int LogLevel = 2; input group "═══ Tester Overrides ═══" input bool TesterMode = true; // Enable relaxed filters for testing input double TesterSpreadMult = 3.0; // Relaxed spread multiplier in tester input double TesterMaxGapPoints = 300.0; // Relaxed gap points in tester input double TesterMinVolatility = 1.0; // Relaxed min volatility in tester input double TesterMaxVolatility = 200.0; // Relaxed max volatility in tester enum MarketState { CLEAN_TREND = 0, CLEAN_BREAKOUT = 1, NOISY_CHOP = 2, HIGH_RISK_NEWS = 3, THIN_LIQUIDITY = 4, EXECUTION_UNSTABLE = 5 }; enum SignalType { SIGNAL_NONE = 0, SIGNAL_BUY = 1, SIGNAL_SELL = -1 }; enum SweepState { SWEEP_NONE = 0, SWEEP_HIGH = 1, SWEEP_LOW = -1 }; #define MAX_TICKS 300 #define MAX_SYMBOLS 20 #define MAX_SPREAD_HIST 300 struct TickData { double bid; double ask; long volume; datetime time; ulong ms; }; struct SymbolState { string symbol; int digits; double point; double tickSize; double tickValue; double lotStep; double minLot; double maxLot; double contractSize; double bid; double ask; double spread; datetime lastTickTime; ulong lastTickMS; double avgSpread; double spreadStdDev; int spreadSpikeCount; double spreadHistory[MAX_SPREAD_HIST]; int spreadHistoryIdx; int spreadHistoryCount; double tickVelocity; int tickCountWindow; datetime tickWindowStart; int tickBurstCount; double lastBid; double lastAsk; double maxGapPoints; int gapEventCount; double microVolatility; double slippageEstimate; double avgSlippage; int slippageSamples; MarketState state; bool noisy; bool tradable; int qualityScore; SweepState sweepState; double sweepLevel; datetime sweepTime; bool noiseSettled; bool structureConfirmed; datetime sweepDetectedAt; double swingHigh; double swingLow; double prevSwingHigh; double prevSwingLow; int tradesThisSession; int blockedByNoise; double sessionPnL; TickData tickBuffer[MAX_TICKS]; int tickIdx; int tickCount; int atrHandle; datetime lastCollectTime; // Track last tick collection time int barsSinceLastTick; // Track bars without ticks }; //--- Global variables CTrade Trade; CPositionInfo PositionInfo; string SymbolArray[]; int SymbolCount = 0; SymbolState States[]; int TotalSignals = 0; int TotalFiltered = 0; int TotalExecuted = 0; datetime SessionStart; double SessionStartBalance; bool IsTesting = false; // Auto-detect tester mode
Для начала мы подключаем библиотеки торговли и управления позициями, которые предоставляют экспертному советнику (EA) доступ к исполнению ордеров, данным по символам и информации об активных сделках. Затем мы организуем систему в виде четко разграниченных групп входных параметров, которые управляют мониторингом символов, фильтрацией шума исполнения, обнаружением свипов ликвидности, управлением рисками и ведением диагностического журнала. Данная конфигурация позволяет отслеживать несколько инструментов — XAUUSD, GBPUSD, USDJPY и USDZAR — и настраивать поведение советника с помощью регулируемых пороговых значений. Эти настройки определяют, как система реагирует на скачки спреда, аномальную скорость тиков, разрывы в котировках и нестабильную волатильность до того, как будет разрешено исполнение любой сделки. Вынося эти настройки во входные параметры, мы создаем гибкую основу, которую можно оптимизировать для различных символов, торговых сессий и условий исполнения.
Остальная часть кода реализует внутренний механизм отслеживания состояния рынка, который советник использует для классификации торговых условий и управления данными по каждому символу в режиме реального времени. Мы определяем несколько перечислений, которые описывают текущую рыночную обстановку, торговые сигналы и направление свипа ликвидности, что позволяет системе принимать структурированные решения, а не полагаться на отдельные значения индикаторов. Структуры TickData и SymbolState хранят данные по каждому символу: статистику спреда, тиковую активность, волатильность, оценки проскальзывания, состояние свипа ликвидности и показатели эффективности за сессию. Наконец, с помощью глобальных переменных инициализируются торговые объекты, массивы символов, счетчики эффективности и логика определения работы в тестере, обеспечивающие согласованную работу всей многопарной системы исполнения.
//+------------------------------------------------------------------+ //| Helper function to detect if running in tester | //+------------------------------------------------------------------+ bool IsTesterMode() { //--- Check if tester is active if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_VISUAL_MODE)) return true; return false; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void LOG(int level, string msg) { if(level <= LogLevel) Print(msg); } //+------------------------------------------------------------------+ //| Get Filling Mode | //+------------------------------------------------------------------+ ENUM_ORDER_TYPE_FILLING GetFillingMode(string sym) { if(sym == "" || SymbolInfoInteger(sym, SYMBOL_SELECT) == 0) return ORDER_FILLING_RETURN; uint filling = (uint)SymbolInfoInteger(sym, SYMBOL_FILLING_MODE); if((filling & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK; if((filling & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC; return ORDER_FILLING_RETURN; } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Auto-detect tester mode IsTesting = IsTesterMode(); LOG(2, "════════════════════════════════════════════"); LOG(2, " Market Microstructure Noise Filter EA "); if(IsTesting) LOG(2, " TESTER MODE - Relaxed Filters Active "); LOG(2, " Liquidity Sweep Continuation Strategy "); LOG(2, "════════════════════════════════════════════"); string rawTokens[]; int rawCount = StringSplit(Symbols, ',', rawTokens); if(rawCount == 0 || rawCount > MAX_SYMBOLS) { LOG(1, "ERROR: Invalid symbol list"); return INIT_FAILED; } ArrayResize(SymbolArray, rawCount); SymbolCount = 0; for(int i = 0; i < rawCount; i++) { string sym = rawTokens[i]; StringTrimLeft(sym); StringTrimRight(sym); if(StringLen(sym) == 0) continue; SymbolArray[SymbolCount++] = sym; } if(SymbolCount == 0) { LOG(1, "ERROR: No valid symbols after trim."); return INIT_FAILED; } Trade.SetExpertMagicNumber(MagicNumber); Trade.SetDeviationInPoints(MaxSlippagePoints); if(SymbolCount > 0 && SymbolArray[0] != "") { ENUM_ORDER_TYPE_FILLING filling = GetFillingMode(SymbolArray[0]); Trade.SetTypeFilling(filling); } else { Trade.SetTypeFilling(ORDER_FILLING_RETURN); } Trade.SetAsyncMode(false); ArrayResize(States, SymbolCount); for(int i = 0; i < SymbolCount; i++) { if(!InitSymbol(i)) { LOG(1, "ERROR: Failed to init symbol: " + SymbolArray[i]); return INIT_FAILED; } LOG(2, " ... " + States[i].symbol + " Digits=" + IntegerToString(States[i].digits) + " AvgSpread≈" + DoubleToString(States[i].avgSpread, 1)); } SessionStart = TimeCurrent(); SessionStartBalance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Use millisecond timer for faster updates if(IsTesting) EventSetMillisecondTimer(100); // 100ms updates in tester else EventSetTimer(1); LOG(2, "═══ EA Online — " + IntegerToString(SymbolCount) + " symbols ═══"); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Symbol Initializer | //+------------------------------------------------------------------+ bool InitSymbol(int idx) { string sym = SymbolArray[idx]; States[idx].symbol = sym; States[idx].digits = 0; States[idx].point = 0; States[idx].tickSize = 0; States[idx].tickValue = 0; States[idx].lotStep = 0; States[idx].minLot = 0; States[idx].maxLot = 0; States[idx].contractSize = 0; States[idx].bid = 0; States[idx].ask = 0; States[idx].spread = 0; States[idx].lastTickTime = 0; States[idx].lastTickMS = 0; States[idx].avgSpread = 0; States[idx].spreadStdDev = 0; States[idx].spreadSpikeCount = 0; States[idx].spreadHistoryIdx = 0; States[idx].spreadHistoryCount = 0; States[idx].tickVelocity = 0; States[idx].tickCountWindow = 0; States[idx].tickWindowStart = TimeCurrent(); States[idx].tickBurstCount = 0; States[idx].lastBid = 0; States[idx].lastAsk = 0; States[idx].maxGapPoints = 0; States[idx].gapEventCount = 0; States[idx].microVolatility = 0; States[idx].slippageEstimate = 0; States[idx].avgSlippage = 0; States[idx].slippageSamples = 0; States[idx].state = THIN_LIQUIDITY; States[idx].noisy = false; // Start as not noisy to allow initial trading States[idx].tradable = true; // Start as tradable States[idx].qualityScore = 70; // Higher initial score States[idx].sweepState = SWEEP_NONE; States[idx].sweepLevel = 0; States[idx].sweepTime = 0; States[idx].noiseSettled = false; States[idx].structureConfirmed = false; States[idx].sweepDetectedAt = 0; States[idx].swingHigh = 0; States[idx].swingLow = 0; States[idx].prevSwingHigh = 0; States[idx].prevSwingLow = 0; States[idx].tradesThisSession = 0; States[idx].blockedByNoise = 0; States[idx].sessionPnL = 0; States[idx].tickIdx = 0; States[idx].tickCount = 0; States[idx].atrHandle = INVALID_HANDLE; States[idx].lastCollectTime = 0; States[idx].barsSinceLastTick = 0; if(!SymbolSelect(sym, true)) { LOG(1, "[" + sym + "] Cannot select — not found in broker symbols."); return false; } double testBid = SymbolInfoDouble(sym, SYMBOL_BID); if(testBid <= 0) { LOG(1, "[" + sym + "] Symbol selected but bid=0 — may not be available."); } States[idx].digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); States[idx].point = SymbolInfoDouble(sym, SYMBOL_POINT); States[idx].tickSize = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); States[idx].tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE); States[idx].lotStep = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP); States[idx].minLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN); States[idx].maxLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX); States[idx].contractSize = SymbolInfoDouble(sym, SYMBOL_TRADE_CONTRACT_SIZE); if(States[idx].point <= 0) States[idx].point = 0.00001; if(States[idx].tickSize <= 0) States[idx].tickSize = States[idx].point; if(States[idx].lotStep <= 0) States[idx].lotStep = 0.01; if(States[idx].minLot <= 0) States[idx].minLot = 0.01; if(States[idx].maxLot <= 0) States[idx].maxLot = 100.0; //--- Seed spread history with current data MqlTick ticks[]; int loaded = CopyTicks(sym, ticks, COPY_TICKS_ALL, 0, SpreadSamplePeriod); if(loaded > 0) { double spreadSum = 0.0; for(int t = 0; t < loaded; t++) { double sp = 0.0; if(States[idx].point > 0) sp = (ticks[t].ask - ticks[t].bid) / States[idx].point; if(sp < 0) sp = 0; spreadSum += sp; if(States[idx].spreadHistoryCount < MAX_SPREAD_HIST) { States[idx].spreadHistory[States[idx].spreadHistoryIdx] = sp; States[idx].spreadHistoryIdx = (States[idx].spreadHistoryIdx + 1) % MAX_SPREAD_HIST; States[idx].spreadHistoryCount++; } } States[idx].avgSpread = (loaded > 0) ? spreadSum / loaded : 0; //--- Seed tick buffer int seedCount = MathMin(loaded, MAX_TICKS); for(int t = loaded - seedCount; t < loaded; t++) { int bi = States[idx].tickIdx; States[idx].tickBuffer[bi].bid = ticks[t].bid; States[idx].tickBuffer[bi].ask = ticks[t].ask; States[idx].tickBuffer[bi].time = (datetime)(ticks[t].time / 1000); States[idx].tickIdx = (bi + 1) % MAX_TICKS; if(States[idx].tickCount < MAX_TICKS) States[idx].tickCount++; } } if(States[idx].avgSpread <= 0) { double liveBid = SymbolInfoDouble(sym, SYMBOL_BID); double liveAsk = SymbolInfoDouble(sym, SYMBOL_ASK); if(liveBid > 0 && liveAsk > 0 && States[idx].point > 0) States[idx].avgSpread = (liveAsk - liveBid) / States[idx].point; else States[idx].avgSpread = 10.0; } States[idx].bid = SymbolInfoDouble(sym, SYMBOL_BID); States[idx].ask = SymbolInfoDouble(sym, SYMBOL_ASK); States[idx].lastBid = States[idx].bid; States[idx].lastAsk = States[idx].ask; if(States[idx].point > 0) States[idx].spread = (States[idx].ask - States[idx].bid) / States[idx].point; States[idx].atrHandle = iATR(sym, PERIOD_H1, ATRPeriod); if(States[idx].atrHandle == INVALID_HANDLE) LOG(2, "[" + sym + "] Warning: could not create ATR handle (may be OK in tester)."); UpdateSwingLevels(idx); return true; }
Этап инициализации начинается с подготовки среды, в которой будет работать EA. Сначала мы создаем вспомогательные функции, которые определяют, запущен ли советник в тестере стратегий, управляют выводом сообщений в журнал и выбирают подходящий тип исполнения ордеров, поддерживаемый брокером. Эти функции помогают системе автоматически адаптировать свое поведение в зависимости от того, происходит ли реальная торговля или тестирование. В функции OnInit() мы выводим информацию о запуске, разбираем список символов, удаляем пробелы по краям и проверяем количество инструментов. После проверки символов мы настраиваем торговый механизм, задавая выбранный magic number, параметры проскальзывания и тип исполнения ордеров, а затем выделяем память для хранения состояний всех символов. Это обеспечивает четкий и упорядоченный процесс запуска, благодаря которому советник полностью готов к работе до начала обработки рыночных данных.
Затем функция InitSymbol() формирует внутреннюю модель данных для каждого отслеживаемого символа отдельно. Мы инициализируем всю статистику исполнения, показатели спреда, состояния свипа ликвидности, показатели волатильности, буферы тиков и переменные отслеживания торговых сессий, чтобы каждый инструмент начинал работу со стабильной базовой конфигурацией. Кроме того, эта функция проверяет доступность инструмента у брокера с помощью SymbolSelect(), получает важные торговые параметры, такие как размер пункта, ограничения объема в лотах, стоимость тика и размер контракта, и подставляет значения по умолчанию, если какие-либо данные отсутствуют. Чтобы с самого начала учитывать условия исполнения, мы предварительно загружаем исторические тиковые данные с помощью CopyTicks() и используем их для начального заполнения истории спреда и буферов тиков, на которых основан механизм фильтрации шума. В заключение мы рассчитываем начальные параметры спреда, создаем дескриптор индикатора ATR для анализа волатильности, обновляем уровни локальных экстремумов, используемые стратегией свипа ликвидности, и подготавливаем символ к мониторингу и исполнению в режиме реального времени.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); for(int i = 0; i < SymbolCount; i++) { if(States[i].atrHandle != INVALID_HANDLE) { IndicatorRelease(States[i].atrHandle); States[i].atrHandle = INVALID_HANDLE; } } PrintSessionStats(); LOG(2, "EA deinitialized. Reason: " + IntegerToString(reason)); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Collect ticks for ALL symbols on every chart tick for(int i = 0; i < SymbolCount; i++) { CollectTick(i); } } //+------------------------------------------------------------------+ //| Tick Collector | //+------------------------------------------------------------------+ void CollectTick(int idx) { //--- Refresh symbol data (critical for tester mode) SymbolInfoDouble(States[idx].symbol, SYMBOL_BID); // Force refresh double newBid = SymbolInfoDouble(States[idx].symbol, SYMBOL_BID); double newAsk = SymbolInfoDouble(States[idx].symbol, SYMBOL_ASK); if(newBid <= 0 || newAsk <= 0) return; //--- Skip if same as last tick (avoid duplicates) if(newBid == States[idx].bid && newAsk == States[idx].ask) return; States[idx].lastCollectTime = TimeCurrent(); //--- Push into circular buffer int bi = States[idx].tickIdx; States[idx].tickBuffer[bi].bid = newBid; States[idx].tickBuffer[bi].ask = newAsk; States[idx].tickBuffer[bi].time = TimeCurrent(); States[idx].tickBuffer[bi].ms = GetTickCount64(); States[idx].tickIdx = (bi + 1) % MAX_TICKS; if(States[idx].tickCount < MAX_TICKS) States[idx].tickCount++; //--- Live spread history double sp = 0; if(States[idx].point > 0) sp = (newAsk - newBid) / States[idx].point; States[idx].spreadHistory[States[idx].spreadHistoryIdx] = sp; States[idx].spreadHistoryIdx = (States[idx].spreadHistoryIdx + 1) % MAX_SPREAD_HIST; if(States[idx].spreadHistoryCount < MAX_SPREAD_HIST) States[idx].spreadHistoryCount++; States[idx].lastBid = States[idx].bid; States[idx].lastAsk = States[idx].ask; States[idx].bid = newBid; States[idx].ask = newAsk; States[idx].spread = sp; States[idx].lastTickTime = TimeCurrent(); States[idx].tickCountWindow++; } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { if(CheckGlobalDrawdown()) return; //--- In tester mode, actively collect ticks for all symbols every timer event if(IsTesting) { for(int i = 0; i < SymbolCount; i++) { //--- Force collect regardless of chart CollectTick(i); } } for(int i = 0; i < SymbolCount; i++) { //--- Update statistics UpdateSpreadStats(i); UpdateTickVelocity(i); UpdateQuoteGap(i); UpdateMicroVolatility(i); UpdateSlippageEstimate(i); //--- Classify state ClassifyMarketState(i); //--- Noise decision with tester overrides States[i].noisy = IsNoisyTesterAware(i); States[i].tradable = !States[i].noisy; //--- Quality score UpdateQualityScore(i); //--- Strategy if(States[i].tradable) { UpdateSwingLevels(i); DetectLiquiditySweep(i); CheckNoiseSettlement(i); CheckStructureShift(i); if(States[i].sweepState != SWEEP_NONE && States[i].noiseSettled && States[i].structureConfirmed) { TotalSignals++; SignalType sig = GetSignal(i); if(sig != SIGNAL_NONE) { if(ValidateRisk(i, sig)) ExecuteTrade(i, sig); else { TotalFiltered++; LOG(2, "[" + States[i].symbol + "] Signal blocked by Risk Manager"); } } } } else { States[i].blockedByNoise++; if(EnableDetailedLog && States[i].blockedByNoise % 30 == 1) LogNoiseBlock(i); } } }
Этапы завершения работы и обработки тиков предназначены для обеспечения стабильной работы экспертного советника (EA) при непрерывном сборе актуальных рыночных данных по нескольким инструментам. Внутри функции OnDeinit() мы останавливаем систему таймеров, освобождаем все дескрипторы индикаторов ATR и выводим итоговую статистику торговли перед тем, как советник будет удален с графика. Это обеспечивает корректное освобождение памяти и ресурсов индикаторов. Функция OnTick() служит сборщиком данных в реальном времени для всей системы. Каждый новый тик на графике запускает цикл по всем отслеживаемым символам, что позволяет советнику поддерживать синхронизацию рыночной информации по всем инструментам, а не полагаться исключительно на символ активного графика. Таким образом создается централизованный процесс мониторинга нескольких пар, который имеет решающее значение для анализа качества исполнения и фильтрации шума.
Функция CollectTick() обновляет внутренние буферы тиков и историю спреда, на которых основан механизм анализа микроструктуры рынка. Мы обновляем цены спроса и предложения, отбрасываем дубликаты тиков и сохраняем новые рыночные данные в кольцевых буферах для эффективного отслеживания рынка с высокой частотой. Одновременно система обновляет показатели спреда, счетчики тиков и временные метки, используемые для анализа волатильности и исполнения.
Функция OnTimer() становится основным механизмом обработки данных советника. Здесь мы рассчитываем статистику спредов, скорость тиков, разрывы в котировках, микроволатильность и оценки проскальзывания, а затем классифицируем текущее состояние рынка. Если рыночная обстановка считается стабильной, мы переходим к стратегии свипа ликвидности: обновляем уровни локальных экстремумов, выявляем свипы ликвидности, подтверждаем изменения структуры рынка и проверяем соблюдение ограничений по риску перед исполнением. Когда на рынке возникает шум, система блокирует торговую активность и записывает диагностическую информацию, обеспечивая работу стратегии только в условиях исполнения с меньшим уровнем шума.
//+------------------------------------------------------------------+ //| TESTER-AWARE NOISE FILTER | //+------------------------------------------------------------------+ bool IsNoisyTesterAware(int idx) { if(!EnableNoiseFilter) return false; //--- Use relaxed thresholds in tester mode double effectiveSpreadMult = IsTesting ? TesterSpreadMult : MaxSpreadMultiplier; double effectiveMaxGap = IsTesting ? TesterMaxGapPoints : MaxGapPoints; double effectiveMinVol = IsTesting ? TesterMinVolatility : MinVolatilityPoints; double effectiveMaxVol = IsTesting ? TesterMaxVolatility : MaxVolatilityPoints; double effectiveMaxVel = IsTesting ? MaxTickVelocity * 2 : MaxTickVelocity; // Double velocity in tester //--- A. Spread expansion if(States[idx].spread > States[idx].avgSpread * effectiveSpreadMult) { LOG(3, "[" + States[idx].symbol + "] NOISE: Spread " + DoubleToString(States[idx].spread, 1) + " vs avg " + DoubleToString(States[idx].avgSpread, 1)); return true; } //--- B. Tick velocity burst (skip) if(!IsTesting || States[idx].tickCountWindow > 5) // Only check if we have data { if(States[idx].tickVelocity > effectiveMaxVel) { LOG(3, "[" + States[idx].symbol + "] NOISE: TickVel " + DoubleToString(States[idx].tickVelocity, 1) + "/s"); return true; } } //--- C. Quote gap (relaxed) if(States[idx].point > 0 && States[idx].lastBid > 0) { double gap = MathAbs(States[idx].bid - States[idx].lastBid) / States[idx].point; if(gap > effectiveMaxGap) { LOG(3, "[" + States[idx].symbol + "] NOISE: Gap " + DoubleToString(gap, 1) + " pts"); return true; } } //--- D. Micro-vol too low (skip) if(!IsTesting || States[idx].tickCount >= VolatilitySampleTicks) { if(States[idx].microVolatility < effectiveMinVol) { LOG(3, "[" + States[idx].symbol + "] NOISE: MicroVol low " + DoubleToString(States[idx].microVolatility, 2)); return true; } } //--- E. Micro-vol too high if(States[idx].microVolatility > effectiveMaxVol) { LOG(3, "[" + States[idx].symbol + "] NOISE: MicroVol high " + DoubleToString(States[idx].microVolatility, 2)); return true; } //--- F. Execution instability if(!IsTesting && States[idx].slippageEstimate > SlippageStabilityFactor) { LOG(3, "[" + States[idx].symbol + "] NOISE: Slippage est=" + DoubleToString(States[idx].slippageEstimate, 3)); return true; } //--- G. State-level block if(States[idx].state == HIGH_RISK_NEWS || States[idx].state == EXECUTION_UNSTABLE) return true; return false; } //+------------------------------------------------------------------+ //| ANALYSIS FUNCTIONS | //+------------------------------------------------------------------+ void UpdateSpreadStats(int idx) { int n = States[idx].spreadHistoryCount; if(n < 5) return; double sum = 0.0; for(int i = 0; i < n; i++) sum += States[idx].spreadHistory[i]; States[idx].avgSpread = sum / n; double varSum = 0.0; for(int i = 0; i < n; i++) { double d = States[idx].spreadHistory[i] - States[idx].avgSpread; varSum += d * d; } States[idx].spreadStdDev = MathSqrt(varSum / n); if(States[idx].spread > States[idx].avgSpread * MaxSpreadMultiplier) States[idx].spreadSpikeCount++; }
Функция IsNoisyTesterAware() выступает в качестве основного фильтра качества исполнения экспертного советника (EA). Его цель — определить, являются ли текущие рыночные условия слишком нестабильными для безопасной торговли. Сначала мы проверяем, включен ли фильтр шума, а затем динамически настраиваем пороги фильтрации в зависимости от того, работает ли советник в режиме реальной торговли или в тестере стратегий. Это важно, поскольку в тестовых средах часто наблюдается искусственное поведение тиков и нереалистичные условия исполнения. Затем функция анализирует ряд микроструктурных сигналов, в том числе расширение спреда, аномальную скорость тиков, значительные разрывы в котировках, низкую или чрезмерную микроволатильность, а также нестабильные оценки проскальзывания. Если какой-либо из этих показателей выходит за пределы допустимых значений, система классифицирует рынок как "шумный" и блокирует торговлю. Кроме того, мы блокируем исполнение в состояниях, классифицированных как высокорисковые, например при нестабильной обстановке на фоне новостей или серьезной нестабильности исполнения.
Функция UpdateSpreadStats() постоянно обновляет статистическую модель спреда, используемую механизмом фильтрации шума. Сначала мы проверяем, накоплено ли достаточно исторических значений спреда, и только затем рассчитываем средний спред и его стандартное отклонение по данным буфера истории. Эти расчеты позволяют советнику определять нормальное поведение спреда для каждого отслеживаемого инструмента, а не опираться на фиксированные допущения. После определения базового значения спреда система сравнивает текущий спред с ожидаемым средним значением с использованием настроенного множителя спреда. Если спред выходит за пределы допустимого диапазона, регистрируется событие резкого скачка спреда. Этот процесс позволяет экспертному советнику (EA) динамически выявлять ухудшение условий исполнения и адаптировать свои торговые решения с учетом качества рынка в режиме реального времени.
//+------------------------------------------------------------------+ //| Update Tick Velocity | //+------------------------------------------------------------------+ void UpdateTickVelocity(int idx) { datetime now = TimeCurrent(); double elapsed = (double)(now - States[idx].tickWindowStart); if(elapsed <= 0) elapsed = 1.0; States[idx].tickVelocity = States[idx].tickCountWindow / elapsed; if(States[idx].tickVelocity > MaxTickVelocity) States[idx].tickBurstCount++; States[idx].tickCountWindow = 0; States[idx].tickWindowStart = now; } //+------------------------------------------------------------------+ //| Update Quote Gap | //+------------------------------------------------------------------+ void UpdateQuoteGap(int idx) { if(States[idx].lastBid <= 0 || States[idx].point <= 0) return; double gapPts = MathAbs(States[idx].bid - States[idx].lastBid) / States[idx].point; if(gapPts > States[idx].maxGapPoints) States[idx].maxGapPoints = gapPts; if(gapPts > MaxGapPoints) { States[idx].gapEventCount++; LOG(3, "[" + States[idx].symbol + "] Quote gap: " + DoubleToString(gapPts, 1) + " pts"); } } //+------------------------------------------------------------------+ //| Update Micro Vol | //+------------------------------------------------------------------+ void UpdateMicroVolatility(int idx) { int n = MathMin(States[idx].tickCount, VolatilitySampleTicks); if(n < 5) return; double prices[]; ArrayResize(prices, n); int start = (States[idx].tickIdx - n + MAX_TICKS) % MAX_TICKS; for(int i = 0; i < n; i++) { int bi = (start + i) % MAX_TICKS; prices[i] = (States[idx].tickBuffer[bi].bid + States[idx].tickBuffer[bi].ask) / 2.0; } double returns[]; ArrayResize(returns, n - 1); double pt = States[idx].point; if(pt <= 0) pt = 0.00001; for(int i = 0; i < n - 1; i++) returns[i] = (prices[i+1] - prices[i]) / pt; double mean = 0.0; for(int i = 0; i < n - 1; i++) mean += returns[i]; mean /= (n - 1); double var = 0.0; for(int i = 0; i < n - 1; i++) { double d = returns[i] - mean; var += d * d; } States[idx].microVolatility = (n > 2) ? MathSqrt(var / (n - 1)) : 0; } //+------------------------------------------------------------------+ //| Update Slippage Estimate | //+------------------------------------------------------------------+ void UpdateSlippageEstimate(int idx) { if(States[idx].avgSpread < 0.5) return; double spreadFactor = States[idx].spread / MathMax(States[idx].avgSpread, 1.0); double velocityFactor = States[idx].tickVelocity / MathMax(MaxTickVelocity, 1.0); double volFactor = States[idx].microVolatility / MathMax(MaxVolatilityPoints, 1.0); States[idx].slippageEstimate = (spreadFactor + velocityFactor + volFactor) / 3.0; }
Первая группа функций предназначена для измерения рыночной активности в режиме реального времени и выявления нестабильности исполнения. В функции UpdateTickVelocity() мы рассчитываем скорость тиков, деля количество собранных тиков на время, прошедшее в пределах окна мониторинга. Это позволяет нам получить оперативную оценку интенсивности рыночной активности по каждому инструменту. Если скорость тиков превышает заданный порог, система фиксирует событие "всплеска тиков", что может свидетельствовать об агрессивном потоке ордеров, волатильности, вызванной новостями, или нестабильной ликвидности.
Затем функция UpdateQuoteGap() измеряет резкие скачки цены спроса между ее последовательными обновлениями. Выражая эти изменения в пунктах, мы можем отслеживать аномальные разрывы в котировках, которые часто возникают при быстрых движениях рынка или в условиях низкой ликвидности. Когда разрыв превышает допустимый предел, советник регистрирует это событие и увеличивает счетчик разрывов в котировках для данного инструмента.
Вторая группа функций анализирует динамику краткосрочной волатильности и оценивает общее качество исполнения на рынке. Внутри функции UpdateMicroVolatility() мы извлекаем последние цены середины спреда из буфера тиков и рассчитываем изменение цены от тика к тику с использованием размера пункта для данного символа. Затем мы вычисляем среднее значение и дисперсию этих доходностей, чтобы получить показатель микроволатильности, отражающий краткосрочную нестабильность цен. Это позволяет советнику отличать устойчивое направленное движение от случайного шума исполнения. Наконец, функция UpdateSlippageEstimate() объединяет динамику спреда, скорость тиков и микроволатильность в единый показатель стабильности исполнения. Путём усреднения этих нормализованных факторов система формирует упрощённую оценку риска проскальзывания, которая помогает определить, подходят ли текущие рыночные условия для надёжного исполнения сделок.
//+------------------------------------------------------------------+ //| Classify Market State | //+------------------------------------------------------------------+ void ClassifyMarketState(int idx) { bool spreadSpike = States[idx].spread > States[idx].avgSpread * MaxSpreadMultiplier; bool tickBurst = States[idx].tickVelocity > MaxTickVelocity; bool quoteGap = States[idx].point > 0 && MathAbs(States[idx].bid - States[idx].lastBid) / States[idx].point > MaxGapPoints; bool lowTick = States[idx].tickVelocity < 0.01; bool highSlippage = States[idx].slippageEstimate > SlippageStabilityFactor; bool choppyVol = States[idx].microVolatility < MinVolatilityPoints || States[idx].microVolatility > MaxVolatilityPoints; if(spreadSpike && tickBurst) States[idx].state = HIGH_RISK_NEWS; else if(highSlippage || spreadSpike) States[idx].state = EXECUTION_UNSTABLE; else if(tickBurst && quoteGap) States[idx].state = HIGH_RISK_NEWS; else if(lowTick) States[idx].state = THIN_LIQUIDITY; else if(choppyVol) States[idx].state = NOISY_CHOP; else if(States[idx].microVolatility > MinVolatilityPoints && !spreadSpike && !tickBurst) { double atr = GetATR(idx); States[idx].state = (atr > 0 && States[idx].microVolatility > atr * 0.5) ? CLEAN_BREAKOUT : CLEAN_TREND; } else States[idx].state = NOISY_CHOP; } //+------------------------------------------------------------------+ //| Update Score | //+------------------------------------------------------------------+ void UpdateQualityScore(int idx) { int score = 70; // Start with high score if(IsTesting) score = 75; double spreadRatio = (States[idx].avgSpread > 0) ? States[idx].spread / States[idx].avgSpread : 1.0; if(spreadRatio > 1.0) score -= (int)MathMin(30.0, (spreadRatio - 1.0) * 30.0); double velRatio = States[idx].tickVelocity / MathMax(MaxTickVelocity, 1.0); if(velRatio > 0.5) score -= (int)MathMin(20.0, velRatio * 20.0); double volMid = (MinVolatilityPoints + MaxVolatilityPoints) / 2.0; double volRange = (MaxVolatilityPoints - MinVolatilityPoints) / 2.0; double volDist = MathAbs(States[idx].microVolatility - volMid) / MathMax(volRange, 1.0); score -= (int)MathMin(25.0, volDist * 25.0); score -= (int)MathMin(15.0, States[idx].slippageEstimate * 10.0); switch(States[idx].state) { case CLEAN_TREND: score += 10; break; case CLEAN_BREAKOUT: score += 5; break; case NOISY_CHOP: score -= 15; break; case HIGH_RISK_NEWS: score -= 40; break; case THIN_LIQUIDITY: score -= 20; break; case EXECUTION_UNSTABLE: score -= 35; break; } States[idx].qualityScore = (int)MathMax(0.0, MathMin(100.0, (double)score)); } //+------------------------------------------------------------------+ //| Update Swings | //+------------------------------------------------------------------+ void UpdateSwingLevels(int idx) { MqlRates rates[]; ArraySetAsSeries(rates, true); int copied = CopyRates(States[idx].symbol, PERIOD_H1, 0, SwingLookback + 5, rates); if(copied < SwingLookback + 3) return; double highVal = 0.0, lowVal = DBL_MAX; double prevHighVal = 0.0, prevLowVal = DBL_MAX; bool foundHigh = false, foundLow = false; for(int i = 1; i < SwingLookback && i < copied - 1; i++) { if(rates[i].high > rates[i-1].high && rates[i].high > rates[i+1].high) { if(!foundHigh) { highVal = rates[i].high; foundHigh = true; } else if(prevHighVal == 0.0) prevHighVal = rates[i].high; } if(rates[i].low < rates[i-1].low && rates[i].low < rates[i+1].low) { if(!foundLow) { lowVal = rates[i].low; foundLow = true; } else if(prevLowVal == DBL_MAX) prevLowVal = rates[i].low; } if(foundHigh && foundLow && prevHighVal > 0 && prevLowVal < DBL_MAX) break; } if(foundHigh) { States[idx].prevSwingHigh = (States[idx].swingHigh > 0) ? States[idx].swingHigh : highVal; States[idx].swingHigh = highVal; } if(foundLow && lowVal < DBL_MAX) { States[idx].prevSwingLow = (States[idx].swingLow > 0) ? States[idx].swingLow : lowVal; States[idx].swingLow = lowVal; } if(prevHighVal > 0) States[idx].prevSwingHigh = prevHighVal; if(prevLowVal < DBL_MAX) States[idx].prevSwingLow = prevLowVal; }
Функция ClassifyMarketState() выступает в качестве механизма принятия решений, который анализирует текущую рыночную ситуацию на основе статистических данных об исполнении ордеров, ранее собранных системой. Мы анализируем ряд микроструктурных сигналов, поступающих в режиме реального времени, в том числе расширение спреда, всплески тиков, разрывы в котировках, активность в условиях низкой ликвидности, нестабильность проскальзывания и аномальное поведение волатильности. На основании совокупности этих условий советник классифицирует рынок по заранее заданным состояниям, таким как HIGH_RISK_NEWS, EXECUTION_UNSTABLE, THIN_LIQUIDITY, NOISY_CHOP, CLEAN_BREAKOUT или CLEAN_TREND. Этот процесс классификации позволяет торговой системе определить, является ли рыночная обстановка безопасной для исполнения сделок или слишком нестабильной для надёжной торговли. Вместо того чтобы одинаково реагировать на все рыночные условия, советник динамически адаптирует своё поведение в зависимости от текущего качества исполнения и условий ликвидности.
Остальные функции позволяют ещё больше уточнить модель исполнения за счёт присвоения оценок качества и отслеживания уровней рыночной структуры, используемых в стратегии свипа ликвидности. В функции UpdateQualityScore() мы создаём динамическую систему оценки, которая повышает балл за стабильные спреды, контролируемую волатильность и более "чистые" состояния рынка и снижает его за нестабильные условия исполнения, такие как чрезмерное проскальзывание, "шумная" волатильность и высокорисковое поведение рынка на фоне новостей. В результате получается числовой показатель качества в диапазоне от 0 до 100, который отражает текущую торговую конъюнктуру для каждого символа. Затем функция UpdateSwingLevels() анализирует данные последних свечей H1 для выявления локальных максимумов и минимумов, которые представляют собой важные зоны ликвидности на рынке. Благодаря сохранению как текущих, так и предыдущих уровней локальных экстремумов советник может впоследствии выявлять свипы ликвидности, изменения структуры рынка и возможности для продолжения движения, которые составляют основу торговой стратегии.
//+------------------------------------------------------------------+ //| Detect Liquidity Sweep | //+------------------------------------------------------------------+ void DetectLiquiditySweep(int idx) { if(States[idx].swingHigh <= 0 || States[idx].swingLow <= 0) return; double tolerance = SweepTolerancePoints * States[idx].point; datetime now = TimeCurrent(); if(States[idx].sweepState != SWEEP_NONE && (now - States[idx].sweepDetectedAt) < (datetime)(NoiseSettleSeconds * 4)) return; if(States[idx].ask > States[idx].swingHigh && States[idx].bid < States[idx].swingHigh + tolerance * 20.0) { States[idx].sweepState = SWEEP_HIGH; States[idx].sweepLevel = States[idx].swingHigh; States[idx].sweepTime = now; States[idx].sweepDetectedAt = now; States[idx].noiseSettled = false; States[idx].structureConfirmed = false; LOG(2, "[" + States[idx].symbol + "] Sweep HIGH @ " + DoubleToString(States[idx].sweepLevel, States[idx].digits)); return; } if(States[idx].bid < States[idx].swingLow && States[idx].ask > States[idx].swingLow - tolerance * 20.0) { States[idx].sweepState = SWEEP_LOW; States[idx].sweepLevel = States[idx].swingLow; States[idx].sweepTime = now; States[idx].sweepDetectedAt = now; States[idx].noiseSettled = false; States[idx].structureConfirmed = false; LOG(2, "[" + States[idx].symbol + "] Sweep LOW @ " + DoubleToString(States[idx].sweepLevel, States[idx].digits)); } } //+------------------------------------------------------------------+ //| Check Noise Filters | //+------------------------------------------------------------------+ void CheckNoiseSettlement(int idx) { if(States[idx].sweepState == SWEEP_NONE) return; if(States[idx].noiseSettled) return; //--- In tester mode, settle immediately after 1 bar int requiredBars = IsTesting ? 1 : 1; datetime barTime = iTime(States[idx].symbol, PERIOD_CURRENT, 0); int barsElapsed = Bars(States[idx].symbol, PERIOD_CURRENT, States[idx].sweepDetectedAt, barTime); if(barsElapsed < requiredBars) return; double spreadLimit = IsTesting ? TesterSpreadMult : 1.8; bool spreadOK = (States[idx].avgSpread <= 0) || (States[idx].spread <= States[idx].avgSpread * spreadLimit); bool velocityOK = IsTesting ? true : States[idx].tickVelocity < MaxTickVelocity; bool volOK = IsTesting ? true : (States[idx].tickCount >= VolatilitySampleTicks && States[idx].microVolatility >= MinVolatilityPoints * 0.5 && States[idx].microVolatility <= MaxVolatilityPoints); if(spreadOK && velocityOK && volOK) { States[idx].noiseSettled = true; LOG(2, "[" + States[idx].symbol + "] Noise settled (" + IntegerToString(barsElapsed) + " bars after sweep)"); } } //+------------------------------------------------------------------+ //| Structural Shift | //+------------------------------------------------------------------+ void CheckStructureShift(int idx) { if(States[idx].sweepState == SWEEP_NONE) return; if(!States[idx].noiseSettled) return; if(States[idx].structureConfirmed) return; ENUM_TIMEFRAMES tf = PERIOD_CURRENT; MqlRates rates[]; ArraySetAsSeries(rates, true); int needed = StructureConfirmBars + 4; int copied = CopyRates(States[idx].symbol, tf, 0, needed, rates); if(copied < needed) { tf = PERIOD_H1; copied = CopyRates(States[idx].symbol, tf, 0, needed, rates); if(copied < needed) return; } if(States[idx].sweepState == SWEEP_LOW) { double refHigh = 0.0; for(int i = 1; i <= StructureConfirmBars; i++) refHigh = MathMax(refHigh, rates[i].high); if(refHigh > 0 && rates[0].close > refHigh) { States[idx].structureConfirmed = true; LOG(2, "[" + States[idx].symbol + "] BUY structure confirmed (LOW sweep)"); } } else if(States[idx].sweepState == SWEEP_HIGH) { double refLow = DBL_MAX; for(int i = 1; i <= StructureConfirmBars; i++) refLow = MathMin(refLow, rates[i].low); if(refLow < DBL_MAX && rates[0].close < refLow) { States[idx].structureConfirmed = true; LOG(2, "[" + States[idx].symbol + "] SELL structure confirmed (HIGH sweep)"); } } }
Этап выявления ликвидности предназначен для обнаружения потенциальных сценариев охоты за стопами и захвата ликвидности вблизи важных уровней локальных экстремумов на рынке. Внутри функции DetectLiquiditySweep() мы сначала проверяем наличие корректно определённых локальных максимумов и минимумов, а затем отслеживаем, не выходит ли цена временно за пределы этих уровней. Если цена Ask поднимается выше локального максимума или цена Bid опускается ниже локального минимума в пределах допустимого отклонения, советник фиксирует свип ликвидности и записывает его направление, ценовой уровень и время обнаружения. Одновременно система сбрасывает флаги затухания шума и подтверждения структуры, поскольку одного свипа ликвидности недостаточно для обоснования сделки. Этот процесс позволяет стратегии выявлять области, в которых участники рынка могли задействовать ликвидность размещённых заявок перед возможным продолжением направленного движения.
Следующие функции проверяют, стала ли рыночная обстановка достаточно стабильной для торговли после события, связанного с ликвидностью. В функции CheckNoiseSettlement() мы дожидаемся завершения как минимум одного бара после свипа ликвидности и оцениваем, нормализовались ли спред, скорость тиков и микроволатильность. Только когда эти условия исполнения стабилизируются, советник отмечает, что рыночный шум затих.
Затем функция CheckStructureShift() подтверждает направление движения, анализируя структуру последних свечей. После свипа ликвидности ниже локального минимума система ожидает пробоя недавних максимумов ценой для подтверждения продолжения восходящего движения, тогда как после свипа ликвидности выше локального максимума требуется закрытие ниже недавних минимумов для подтверждения продолжения нисходящего движения. После затухания шума и получения структурного подтверждения советник может с гораздо большей уверенностью считать, что за свипом ликвидности последовало более чёткое продолжение движения, а не случайный шум исполнения.
//+------------------------------------------------------------------+ //| Signal Type | //+------------------------------------------------------------------+ SignalType GetSignal(int idx) { if(States[idx].sweepState == SWEEP_NONE) return SIGNAL_NONE; if(!States[idx].noiseSettled) return SIGNAL_NONE; if(!States[idx].structureConfirmed) return SIGNAL_NONE; if(TimeCurrent() - States[idx].sweepDetectedAt > 14400) { ResetSweepState(idx); return SIGNAL_NONE; } if(CountPositions(States[idx].symbol) > 0) return SIGNAL_NONE; SignalType sig = SIGNAL_NONE; if(States[idx].sweepState == SWEEP_LOW && EnableBuySignals) sig = SIGNAL_BUY; if(States[idx].sweepState == SWEEP_HIGH && EnableSellSignals) sig = SIGNAL_SELL; //--- Lower score threshold in tester int scoreThreshold = IsTesting ? 50 : 55; if(sig != SIGNAL_NONE && States[idx].qualityScore < scoreThreshold) { LOG(2, "[" + States[idx].symbol + "] Score too low (" + IntegerToString(States[idx].qualityScore) + ") — skip"); return SIGNAL_NONE; } if(sig != SIGNAL_NONE) LOG(2, "[" + States[idx].symbol + "] Signal: " + (sig == SIGNAL_BUY ? "BUY" : "SELL") + " Score=" + IntegerToString(States[idx].qualityScore)); return sig; } void ResetSweepState(int idx) { States[idx].sweepState = SWEEP_NONE; States[idx].sweepLevel = 0; States[idx].noiseSettled = false; States[idx].structureConfirmed = false; } //+------------------------------------------------------------------+ //| RISK & EXECUTION | //+------------------------------------------------------------------+ bool ValidateRisk(int idx, SignalType sig) { string sym = States[idx].symbol; if(CountAllPositions() >= MaxTotalPositions) { LOG(2, "[" + sym + "] Risk: max positions"); return false; } if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) < 100) { LOG(1, "[" + sym + "] Risk: low margin"); return false; } return true; } //+------------------------------------------------------------------+ //| Calculate Lots | //+------------------------------------------------------------------+ double CalcLotSize(int idx) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double atr = GetATR(idx); double pt = States[idx].point; if(pt <= 0) pt = 0.00001; if(atr <= 0) atr = 50 * pt; double slDist = atr * StopLossATRMultiplier; double riskAmt = balance * (RiskPercent / 100.0); double tickVal = States[idx].tickValue; double tickSz = States[idx].tickSize; double pipValue = (tickSz > 0 && tickVal > 0) ? (tickVal / tickSz) * pt : 10.0; if(pipValue <= 0) pipValue = 10.0; double slPips = slDist / pt; if(slPips <= 0) slPips = 1.0; double rawLot = riskAmt / (slPips * pipValue); rawLot = MathFloor(rawLot / States[idx].lotStep) * States[idx].lotStep; rawLot = MathMax(States[idx].minLot, MathMin(States[idx].maxLot, rawLot)); return NormalizeDouble(rawLot, 2); } //+------------------------------------------------------------------+ //| Check Drawdown | //+------------------------------------------------------------------+ bool CheckGlobalDrawdown() { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); if(balance <= 0) return false; double ddPct = ((balance - equity) / balance) * 100.0; if(ddPct >= MaxDrawdownPercent) { LOG(1, "GLOBAL DD HALT: " + DoubleToString(ddPct, 2) + "%"); return true; } return false; } //+------------------------------------------------------------------+ //| Execute Trade | //+------------------------------------------------------------------+ void ExecuteTrade(int idx, SignalType sig) { string sym = States[idx].symbol; double atr = GetATR(idx); if(atr <= 0) { LOG(1, "[" + sym + "] No ATR — skip"); return; } double slDist = atr * StopLossATRMultiplier; double tpDist = slDist * TakeProfitRR; double lots = CalcLotSize(idx); int digits = States[idx].digits; double point = States[idx].point; int stopLvl = (int)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL); double minDist = stopLvl * point; slDist = MathMax(slDist, minDist + 2 * point); tpDist = MathMax(tpDist, minDist + 2 * point); for(int attempt = 0; attempt < MaxRetries; attempt++) { double freshBid = SymbolInfoDouble(sym, SYMBOL_BID); double freshAsk = SymbolInfoDouble(sym, SYMBOL_ASK); double price = 0, sl = 0, tp = 0; if(sig == SIGNAL_BUY) { price = freshAsk; sl = NormalizeDouble(price - slDist, digits); tp = NormalizeDouble(price + tpDist, digits); if(Trade.Buy(lots, sym, price, sl, tp, "MMEF_BUY|Sc=" + IntegerToString(States[idx].qualityScore))) { TotalExecuted++; States[idx].tradesThisSession++; ResetSweepState(idx); LOG(2, "[" + sym + "] BUY executed! Lots=" + DoubleToString(lots,2)); break; } } else { price = freshBid; sl = NormalizeDouble(price + slDist, digits); tp = NormalizeDouble(price - tpDist, digits); if(Trade.Sell(lots, sym, price, sl, tp, "MMEF_SELL|Sc=" + IntegerToString(States[idx].qualityScore))) { TotalExecuted++; States[idx].tradesThisSession++; ResetSweepState(idx); LOG(2, "[" + sym + "] SELL executed! Lots=" + DoubleToString(lots,2)); break; } } LOG(1, "[" + sym + "] Attempt " + IntegerToString(attempt+1) + " failed. Code=" + IntegerToString((int)Trade.ResultRetcode())); Sleep(RetryDelayMS); } } //+------------------------------------------------------------------+
Этап формирования сигнала гарантирует, что сделки инициируются только после полного подтверждения всех условий по свипу ликвидности и качеству исполнения. В функции GetSignal() мы сначала проверяем, выявлен ли свип ликвидности, отвечающий заданным условиям, затих ли рыночный шум и получено ли уже структурное подтверждение. Кроме того, мы отбрасываем торговые формации свипа ликвидности с истёкшим сроком действия и предотвращаем повторные входы, если по данному инструменту уже открыта позиция. Как только эти условия выполняются, функция преобразует обнаруженный свип ликвидности в сигнал на покупку или продажу в зависимости от направления свипа и включённых торговых настроек.
Перед тем как разрешить исполнение, советник проверяет, достигает ли оценка качества инструмента требуемого порога, чтобы сделки инициировались только в рыночных условиях достаточно высокого качества. Если торговая формация проходит все фильтры, система записывает в журнал итоговый сигнал вместе с оценкой качества исполнения. Функция ResetSweepState() используется для очистки всей информации, связанной со свипом ликвидности, когда торговая формация становится недействительной или сделка уже исполнена.
Уровень управления рисками и исполнения сделок направлен на защиту счета при одновременном обеспечении стабильного размещения торговых ордеров. В функции ValidateRisk() мы проверяем, что на счете имеется достаточный объем свободной маржи и что общее количество открытых позиций не превышает заданных лимитов. Затем функция CalcLotSize() динамически рассчитывает размер позиции с учетом баланса счета, расстояния до стопа, рассчитанного на основе ATR, стоимости тика и процентной доли риска. Это позволяет советнику автоматически адаптировать размер лота в зависимости от волатильности инструмента и состояния счета.
Функция CheckGlobalDrawdown() выступает в качестве механизма защиты портфеля, приостанавливая торговую деятельность в случае, если просадка счета превышает максимально допустимый процент. Наконец, функция ExecuteTrade() рассчитывает уровни SL/TP на основе ATR, обновляет цены, проверяет ограничения брокера на установку стоп-уровней и отправляет ордер, при необходимости повторяя попытки. После успешного исполнения сделки система фиксирует это событие, обновляет статистику сессии и сбрасывает состояние активного свипа ликвидности для данного инструмента.
Бэктест
Бэктест проводился в течение примерно двухмесячного периода с 2 февраля 2026 года по 1 апреля 2026 года с использованием настроек по умолчанию:


Заключение
В этой статье мы с нуля создали EA профессионального уровня на MQL5. Мы начали с многосимвольной архитектуры, в которой каждый инструмент обрабатывается независимо: для него ведутся собственный буфер тиков, история спреда и состояние рынка. Мы добавили шесть фильтров шума, работающих в реальном времени: отслеживание расширения спреда, скорости тиков, разрывов в котировках и микроволатильности, оценку проскальзывания и классификацию состояния исполнения — каждый из них выявляет конкретный тип рыночных условий, при которых исполнение становится небезопасным.
Кроме того, мы разработали стратегию продолжения движения после свипа ликвидности, которая дожидается завершения "охоты за стопами", подтверждает затухание шума и открывает позицию только тогда, когда структура рынка меняется в нужном направлении. Затем мы объединили всё это в многоуровневый механизм управления рисками, который одновременно по всем парам защищает от рисков, связанных с коррелированными позициями, превышения допустимой просадки и чрезмерной нагрузки на маржу.
В результате трейдер полностью меняет своё представление о том, как происходит исполнение торговых операций. Сам советник представляет собой рабочий инструмент — достаточно разместить его на графике, и он сразу же начнёт оценивать качество рынка и отфильтровывать шум, который незаметно подрывает эффективность большинства автоматических стратегий. Но что ещё важнее, у трейдера теперь есть ментальная модель, которая позволяет отделить качество сигнала от качества исполнения, и он понимает, что это две разные проблемы, требующие двух разных решений. Все описанные в статье решения модульны: фильтры шума можно встроить в любой существующий советник, а логику обнаружения свипов ликвидности — адаптировать к любой стратегии, основанной на ликвидности. Трейдер получает не просто код, но и основу, которую можно применять, модифицировать и развивать при разработке каждой последующей системы.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22772
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Метаразметка классических стратегий (Часть 2): Фильтрация сделок по ADX и определение размера позиций
Конструирование признаков для машинного обучения (Часть 6): Признаки микроструктуры рынка в MQL5
Индикатор скользящего коэффициента Шарпа с доверительными границами в MQL5
Адаптивное сглаживание цен в MQL5: фильтр Калмана с оценкой шума без внешних библиотек
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования