Разработка инструментария для анализа Price Action (Часть 62): Создание адаптивной системы обнаружения параллельных каналов и пробоев на языке MQL5
Содержание
- Введение
- Параллельные каналы и структурные пробои
- Архитектура системы на языке MQL5
- Тестирование и результаты
- Заключение
Введение
В предыдущей части этой серии мы рассмотрели систему выявления пробоя трендовой линии с подтверждением по трем точкам свинга. Тестирование показало, что этот подход эффективно выявляет явные возможности для входа на пробое. Однако выявилось одно ограничение: система фиксировала только пробои в направлении строящихся трендовых линий. Это означало, что при движении цены в противоположную сторону или по неотслеживаемой границе такие структуры пробоя оставались незамеченными.
В этой статье подход расширяется за счет добавления адаптивной системы обнаружения параллельных каналов и пробоев. Система строит каналы по точкам свинга, отслеживает Price Action внутри этих каналов и динамически подтверждает пробои. Контроль обеих границ канала позволяет не упускать значимые движения цены.
Основной акцент сделан на сочетании геометрии трендовых линий, адаптивного пересчета и проверки в реальном времени, чтобы предоставить пользователям MQL5 надежный инструментарий для анализа Price Action. Эта система также закладывает основу для дальнейших улучшений, таких как интеграция фильтров волатильности, корректировок на основе ATR и более продвинутых методов подтверждения пробоев.
Параллельные каналы и структурные пробои
Параллельный канал – это графический паттерн, образованный двумя равноудаленными трендовыми линиями: одна соединяет максимумы свингов, другая – минимумы свингов, а вместе они ограничивают движение цены в пределах структурированного диапазона. Параллельные каналы важны для технического анализа, поскольку показывают краткосрочную и среднесрочную направленность рынка и позволяют трейдерам заранее выделять потенциальные зоны пробоя.
Пробои возникают, когда цена уверенно выходит за границы канала вверх или вниз. Структурные пробои особенно важны, поскольку часто сигнализируют о продолжении или развороте преобладающего тренда. Для надежного выявления таких пробоев необходимы как точное построение канала, так и механизмы подтверждения в реальном времени.
Построение канала на основе трендовых линий
В основе системы лежит точное определение структурно значимых максимумов и минимумов свингов. Вместо того чтобы учитывать каждое мелкое колебание, процесс обнаружения использует фильтрацию на основе ATR, чтобы отсекать несущественные движения цены. Это гарантирует, что в расчет берутся только свинги с достаточной амплитудой, что снижает шум и сохраняет структурную целостность.
После выявления подтвержденных точек свинга верхняя граница канала строится путем соединения последовательных максимумов свингов. Эта линия задает направленную геометрию структуры. Затем параллельно строится соответствующая нижняя граница со смещением на равное расстояние, рассчитанным по соотношениям между свингами. В результате получается геометрически согласованный параллельный канал, отражающий реальную структуру рынка, а не произвольное построение.

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

После подтверждения события пробоя отмечаются неперерисовывающимися стрелками. Эти маркеры постоянно сохраняются на графике, благодаря чему исторические сигналы остаются видимыми для анализа и последующего разбора. Разделение между адаптивным пересчетом канала и постоянным хранением сигналов необходимо для сохранения аналитической надежности.
Адаптивное поведение и пересчет в реальном времени
Финансовые рынки динамичны, и любой структурный инструмент должен к этому адаптироваться. Поэтому построение канала пересчитывается по мере поступления новых ценовых данных. Система работает по закрытым барам, чтобы сохранять согласованность и не допускать нестабильности в реальных условиях.
Каждый пересчет заново проверяет корректность свингов, при необходимости перестраивает геометрию канала и повторно оценивает условия пробоя относительно обновленной структуры. Если предыдущий канал остается структурно корректным, он сохраняется. Если новое поведение цены делает его некорректным, строится новая структура. Условия пробоя также постоянно перепроверяются с учетом меняющегося канала. Это предотвращает преждевременные сигналы и при этом не дает пропустить значимые структурные сдвиги. За счет одновременного отслеживания верхней и нижней границ система избегает направленного перекоса. Движения вверх и вниз анализируются с одинаковой тщательностью, что дает полное структурное представление Price Action вместо односторонней интерпретации.

Этот адаптивный подход позволяет системе работать как сканер рыночной структуры в реальном времени: он реагирует на изменения рынка и при этом поддерживает надежность сигналов.
Архитектура системы на языке MQL5
Обзор системы
В этом разделе описана внутренняя организация советника: показано, как его компоненты организованы по модульному принципу, как они взаимодействуют и как поток данных преобразует исходные рыночные данные в подтвержденные сигналы пробоя. Архитектура делает акцент на ясности, удобстве сопровождения и надежности, обеспечивая точный анализ и гибкую настройку. Советник построен по высокомодульной событийно-ориентированной схеме, оптимизированной для MetaTrader 5: каждый компонент работает независимо и отвечает за свою задачу, но при этом согласованно взаимодействует с остальными, формируя целостную систему.
На каждом новом баре система заново строит параллельный канал по недавним точкам свинга – значимым разворотным точкам рынка, – а затем проверяет, соответствует ли эта структура всем заранее заданным критериям. Процесс проверки включает несколько фильтров: согласованность наклона, частоту касаний, давность/свежесть структуры и условия волатильности, причем все они нормализуются на основе 14-периодного ATR. Эта нормализация обеспечивает сопоставимую работу системы на разных символах и таймфреймах с автоматической адаптацией к волатильности рынка. Как только выявлен надежный канал, он отображается на графике в виде четких продлеваемых трендовых линий и меток, сразу предоставляя трейдеру необходимый визуальный контекст. Затем система оценивает, указывает ли недавняя динамика цены на пробой – событие, которое может привести к открытию сделки, – для чего проецирует линии канала на данные в реальном времени, применяет несколько фильтров достоверности и управляет сигналами так, чтобы сократить число ложных срабатываний.
Модульные компоненты и их задачи
Модуль обнаружения точек свинга
Отвечает за анализ исходных ценовых данных с целью выявления значимых локальных максимумов и минимумов, которые формируют структурную основу всего анализа. Он анализирует недавние ценовые бары в настраиваемом диапазоне – обычно последние 150 баров – и применяет фрактальную логику, чтобы выявить пики и впадины, которые заметно выделяются на фоне соседних точек. Для этого проверяется, выходит ли максимум или минимум бара за пределы значений соответствующих соседних баров в пределах окна проверки (SwingLookback). Чтобы не реагировать на незначительные колебания, можно включить дополнительный фильтр ATR, который будет учитывать только те свинги, размер которых превышает минимальный порог, зависящий от текущей волатильности (SwingSizeATRFactor). Обнаруженные точки свинга сохраняются в отдельных массивах, сортируются по хронологии и получают порядковые номера, что упрощает обращение к ним в последующих модулях. Эти точки свинга могут визуально отмечаться стрелками и метками, что помогает трейдеру с первого взгляда оценивать текущую структуру рынка.
//+------------------------------------------------------------------+ //| Detect significant swing highs and lows | //+------------------------------------------------------------------+ void FindSignificantSwings(const MqlRates &rates[],int totalBars, SwingPoint &highs[],int &highCount, SwingPoint &lows[],int &lowCount) { highCount = 0; lowCount = 0; double minSize = UseATRFiltering ? currentATR * SwingSizeATRFactor : MinSwingSize; for(int i = SwingLookback; i < totalBars - SwingLookback; i++) { //--- Detect Swing High bool isHigh = true; double currentHigh = rates[i].high; for(int j=1; j<=SwingLookback; j++) { if(rates[i-j].high >= currentHigh || rates[i+j].high >= currentHigh) { isHigh = false; break; } } if(isHigh) { double leftLow = MathMin(rates[i-1].low,rates[i-2].low); double rightLow = MathMin(rates[i+1].low,rates[i+2].low); double swingSize = currentHigh - MathMax(leftLow,rightLow); if(swingSize >= minSize && highCount < 50) { highs[highCount].time = rates[i].time; highs[highCount].price = currentHigh; highs[highCount].barIndex = i; highs[highCount].size = swingSize; highs[highCount].isHigh = true; highCount++; } } //--- Detect Swing Low bool isLow = true; double currentLow = rates[i].low; for(int j=1; j<=SwingLookback; j++) { if(rates[i-j].low <= currentLow || rates[i+j].low <= currentLow) { isLow = false; break; } } if(isLow) { double leftHigh = MathMax(rates[i-1].high,rates[i-2].high); double rightHigh = MathMax(rates[i+1].high,rates[i+2].high); double swingSize = MathMin(leftHigh,rightHigh) - currentLow; if(swingSize >= minSize && lowCount < 50) { lows[lowCount].time = rates[i].time; lows[lowCount].price = currentLow; lows[lowCount].barIndex = i; lows[lowCount].size = swingSize; lows[lowCount].isHigh = false; lowCount++; } } } if(DebugMode) Print("Swings found: Highs=",highCount," Lows=",lowCount); } //+------------------------------------------------------------------+
Модуль построения канала и оценки
Этот ключевой модуль выполняет полный перебор комбинаций максимумов и минимумов свинга, чтобы найти наиболее надежный и структурно корректный параллельный канал. Он оценивает каждый вариант по нескольким критериям, включая сходство наклона и ширину канала, применяя фильтры ранней отбраковки, такие как MaxSlopeDifference и MinChannelWidthATR. Для каждого варианта улучшенная функция CountTouchesOnLine() подсчитывает касания верхней и нижней трендовых линий, учитывая только те свинги, которые лежат на линии или рядом с ней в пределах допуска и соответствуют положению этой линии.
Количество касаний – ключевой показатель того, насколько хорошо вариант соответствует текущей структуре рынка. Оценка складывается из общего числа касаний, бонусов за актуальность и индикаторов силы, основанных на том, сколько недавних свингов взаимодействуют с линиями. В качестве наилучшего канала выбирается вариант с наивысшей оценкой, который удовлетворяет всем минимальным критериям и отражает наиболее значимую зону поддержки или сопротивления в недавней динамике рынка.
//+------------------------------------------------------------------+ //| Find the best parallel channel from swing points | //+------------------------------------------------------------------+ bool FindBestChannel(const SwingPoint &highs[],int highCount, const SwingPoint &lows[],int lowCount, const MqlRates &rates[],int totalBars, Channel &bestChannel) { bestChannel.score = 0.0; if(highCount < 2 || lowCount < 2) { if(DebugMode) Print("Not enough swings: highs=",highCount," lows=",lowCount); return false; } double tolerance = TouchToleranceATR * currentATR; for(int hi1=0; hi1<highCount-1; hi1++) { for(int hi2=hi1+1; hi2<highCount; hi2++) { double dt_high = (double)(highs[hi2].time - highs[hi1].time); if(dt_high <= 0) continue; double slope_high = (highs[hi2].price - highs[hi1].price) / dt_high; for(int lo1=0; lo1<lowCount-1; lo1++) { for(int lo2=lo1+1; lo2<lowCount; lo2++) { double dt_low = (double)(lows[lo2].time - lows[lo1].time); if(dt_low <= 0) continue; double slope_low = (lows[lo2].price - lows[lo1].price) / dt_low; double avgSlope = (MathAbs(slope_high) + MathAbs(slope_low)) / 2.0; double diff = MathAbs(slope_high - slope_low); double maxDiff = MaxSlopeDifference * MathMax(avgSlope,0.000001); if(diff > maxDiff) continue; double upper_at_l1 = highs[hi1].price + slope_high * (lows[lo1].time - highs[hi1].time); double upper_at_l2 = highs[hi1].price + slope_high * (lows[lo2].time - highs[hi1].time); if(upper_at_l1 <= lows[lo1].price || upper_at_l2 <= lows[lo2].price) continue; double dist1 = upper_at_l1 - lows[lo1].price; double dist2 = upper_at_l2 - lows[lo2].price; double width = (dist1 + dist2) / 2.0; if(width < MinChannelWidthATR * currentATR) continue; double lower_at_l1 = upper_at_l1 - width; double lower_at_l2 = upper_at_l2 - width; if(lower_at_l1 > lows[lo1].price + tolerance || lower_at_l2 > lows[lo2].price + tolerance) continue; int recentHigh = 0; int touchesHigh = CountTouchesOnLine(highs[hi1],highs[hi2],highs,highCount,true,tolerance,RecentTouchBars,recentHigh); SwingPoint lowerAnchor1, lowerAnchor2; lowerAnchor1.time = highs[hi1].time; lowerAnchor1.price = highs[hi1].price - width; lowerAnchor2.time = highs[hi2].time; lowerAnchor2.price = highs[hi2].price - width; int recentLow = 0; int touchesLow = CountTouchesOnLine(lowerAnchor1,lowerAnchor2,lows,lowCount,false,tolerance,RecentTouchBars,recentLow); if(touchesHigh < MinTouchPointsRequired || touchesLow < MinTouchPointsRequired) continue; if(MinRecentTouches > 0 && (recentHigh < MinRecentTouches || recentLow < MinRecentTouches)) continue; double score = (touchesHigh + touchesLow) * 25.0; double recencyBonus = (1.0 - (double)MathMin(highs[hi2].barIndex,lows[lo2].barIndex) / totalBars) * 30.0; score += recencyBonus; if(score > bestChannel.score) { bestChannel.high1 = highs[hi1]; bestChannel.high2 = highs[hi2]; bestChannel.low1 = lows[lo1]; bestChannel.low2 = lows[lo2]; bestChannel.slope = slope_high; bestChannel.width = width; bestChannel.highTouches = touchesHigh; bestChannel.lowTouches = touchesLow; bestChannel.score = score; bestChannel.type = ClassifyChannel(slope_high); bestChannel.strength = GradeStrength(touchesHigh + touchesLow); if(DebugMode) Print(StringFormat("Candidate accepted: H-touches=%d, L-touches=%d, width=%g, score=%g", touchesHigh,touchesLow,width,score)); } } } } } return (bestChannel.score > 0.0); } //+------------------------------------------------------------------+
В процессе поиска канала комбинации максимумов и минимумов свинга оцениваются с помощью вложенных циклов. Хотя это теоретически приводит к квадратичной сложности, на практике производительность остается под контролем благодаря жестким фильтрам ранней отбраковки. Такие условия, как максимальное отклонение наклона и минимальная ширина канала, нормализованная по ATR, исключают слабые варианты еще до более глубокой оценки. Кроме того, ограничение обнаружения свингов рамками ограниченного окна проверки предотвращает неконтролируемый рост числа рассматриваемых комбинаций. Такая структурированная отсечка гарантирует, что движок сохраняет высокую производительность даже во время волатильных фаз рынка.
Модуль визуализации
После определения лучшего канала данный компонент наглядно отображает его на графике. Он строит две параллельные трендовые линии: верхнюю, соединяющую исходный максимум свинга с проецируемой точкой в будущем, и нижнюю, смещенную на ширину канала с сохранением параллельности. Эти линии продлеваются вперед на настраиваемое число баров (LineExtensionBars), формируя перспективную зону поддержки/сопротивления. Трендовые линии динамически пересоздаются на каждом тике, чтобы оставаться синхронизированными с данными в реальном времени и не засорять график устаревшей разметкой. Для повышения наглядности добавляются дополнительные визуальные элементы: кресты в точках свинга и метки с количеством касаний, силой канала и сигналами направленности ("BUY ONLY", "SELL ONLY" или "NEUTRAL"). Все графические объекты получают префикс TL_PREFIX и активно управляются: на каждом тике они удаляются и создаются заново, чтобы график оставался визуально чистым и точным.
//+------------------------------------------------------------------+ //| Draw the parallel channel with extension | //+------------------------------------------------------------------+ void DrawChannel(const Channel &ch) { string upName = TL_PREFIX + "UPPER"; string dnName = TL_PREFIX + "LOWER"; string lblName = TL_PREFIX + "LABEL"; color upperColor = UpperLineColor; color lowerColor = LowerLineColor; //--- Compute future extension point datetime lastSwingTime = MathMax(ch.high2.time,ch.low2.time); datetime futureTime = lastSwingTime + LineExtensionBars * PeriodSeconds(_Period); double futureUpper = LinePriceAtTime(ch.high1,ch.high2,futureTime); double futureLower = futureUpper - ch.width; //--- Upper line (from first high to future point) if(ObjectFind(0,upName) < 0) ObjectCreate(0,upName,OBJ_TREND,0,ch.high1.time,ch.high1.price,futureTime,futureUpper); else { ObjectMove(0,upName,0,ch.high1.time,ch.high1.price); ObjectMove(0,upName,1,futureTime,futureUpper); } ObjectSetInteger(0,upName,OBJPROP_COLOR,upperColor); ObjectSetInteger(0,upName,OBJPROP_WIDTH,LineWidth); ObjectSetInteger(0,upName,OBJPROP_STYLE,LineStyle); ObjectSetInteger(0,upName,OBJPROP_RAY_RIGHT,false); ObjectSetInteger(0,upName,OBJPROP_BACK,false); //--- Lower line (parallel, starting at same time as upper) double lower1 = ch.high1.price - ch.width; if(ObjectFind(0,dnName) < 0) ObjectCreate(0,dnName,OBJ_TREND,0,ch.high1.time,lower1,futureTime,futureLower); else { ObjectMove(0,dnName,0,ch.high1.time,lower1); ObjectMove(0,dnName,1,futureTime,futureLower); } ObjectSetInteger(0,dnName,OBJPROP_COLOR,lowerColor); ObjectSetInteger(0,dnName,OBJPROP_WIDTH,LineWidth); ObjectSetInteger(0,dnName,OBJPROP_STYLE,LineStyle); ObjectSetInteger(0,dnName,OBJPROP_RAY_RIGHT,false); ObjectSetInteger(0,dnName,OBJPROP_BACK,false); //--- Optional low reference crosses if(ShowLowReferenceCrosses) { string cross1 = TL_PREFIX + "LOWREF1", cross2 = TL_PREFIX + "LOWREF2"; if(ObjectFind(0,cross1) < 0) ObjectCreate(0,cross1,OBJ_ARROW,0,ch.low1.time,ch.low1.price); else ObjectMove(0,cross1,0,ch.low1.time,ch.low1.price); ObjectSetInteger(0,cross1,OBJPROP_ARROWCODE,159); ObjectSetInteger(0,cross1,OBJPROP_COLOR,clrGray); ObjectSetInteger(0,cross1,OBJPROP_WIDTH,1); ObjectSetInteger(0,cross1,OBJPROP_BACK,false); if(ObjectFind(0,cross2) < 0) ObjectCreate(0,cross2,OBJ_ARROW,0,ch.low2.time,ch.low2.price); else ObjectMove(0,cross2,0,ch.low2.time,ch.low2.price); ObjectSetInteger(0,cross2,OBJPROP_ARROWCODE,159); ObjectSetInteger(0,cross2,OBJPROP_COLOR,clrGray); ObjectSetInteger(0,cross2,OBJPROP_WIDTH,1); ObjectSetInteger(0,cross2,OBJPROP_BACK,false); } //--- Label datetime labelTime = MathMax(ch.high2.time,ch.low2.time); double labelPrice = (ch.high1.price + ch.high2.price)/2.0 - ch.width/2.0; string strengthText = (ch.strength == HIGH ? "HIGH" : (ch.strength == MEDIUM ? "MEDIUM" : "LOW")); string biasText = "NEUTRAL"; if(UseChannelTypeFilter) { if(ch.type == ASCENDING) biasText = "BUY ONLY"; else if(ch.type == DESCENDING) biasText = "SELL ONLY"; } string labelText = "Touches: " + IntegerToString(ch.highTouches + ch.lowTouches) + "\nStrength: " + strengthText + "\nBias: " + biasText; if(ObjectFind(0,lblName) < 0) ObjectCreate(0,lblName,OBJ_TEXT,0,labelTime,labelPrice); else ObjectMove(0,lblName,0,labelTime,labelPrice); ObjectSetString(0,lblName,OBJPROP_TEXT,labelText); ObjectSetInteger(0,lblName,OBJPROP_COLOR,clrWhite); ObjectSetInteger(0,lblName,OBJPROP_FONTSIZE,10); ObjectSetInteger(0,lblName,OBJPROP_BACK,false); } //+------------------------------------------------------------------+
Проверка пробоя основана на структурированной модели состояний, чтобы исключить дублирование сигналов и их преждевременное формирование. Каждое обнаруженное событие проходит через заданные этапы:
- Neutral – активность пробоя отсутствует;
- Pending Breakout – обнаружено первое пересечение, но до подтверждения ожидается ретест (если включен режим ретеста);
- Confirmed Breakout – все фильтры пройдены, сигнал выдан;
- Expired/Invalidated – до подтверждения условия перестали выполняться.
//+------------------------------------------------------------------+ //| Checks for breakout crossing and applies filters | //+------------------------------------------------------------------+ void CheckChannelBreakouts(const MqlRates &rates[],const Channel &ch, const SwingPoint &highs[],int highCount, const SwingPoint &lows[],int lowCount) { if(ArraySize(rates) < 3) return; datetime t1 = rates[1].time; datetime t2 = rates[2].time; double buf = BreakoutBufferPrice(); double minStrength = BreakoutMinStrengthATR * currentATR; double strongThreshold = BreakoutStrongThreshold * currentATR; int totalTouches = ch.highTouches + ch.lowTouches; double upper1 = LinePriceAtTime(ch.high1,ch.high2,t1); double upper2 = LinePriceAtTime(ch.high1,ch.high2,t2); double pUp1 = BreakoutUseClose ? rates[1].close : rates[1].high; double pUp2 = BreakoutUseClose ? rates[2].close : rates[2].high; bool crossedUp = false; if(BreakoutStrictMode) crossedUp = (pUp2 <= upper2 + buf) && (pUp1 > upper1 + buf); else crossedUp = (pUp1 > upper1 + buf); double upStrength = pUp1 - (upper1 + buf); bool strongUp = (minStrength == 0) || (upStrength >= minStrength); bool superStrongUp = (strongThreshold > 0) && (upStrength >= strongThreshold); double lower1 = upper1 - ch.width; double lower2 = upper2 - ch.width; double pLow1 = BreakoutUseClose ? rates[1].close : rates[1].low; double pLow2 = BreakoutUseClose ? rates[2].close : rates[2].low; bool crossedDown = false; if(BreakoutStrictMode) crossedDown = (pLow2 >= lower2 - buf) && (pLow1 < lower1 - buf); else crossedDown = (pLow1 < lower1 - buf); double downStrength = (lower1 - buf) - pLow1; bool strongDown = (minStrength == 0) || (downStrength >= minStrength); bool superStrongDown = (strongThreshold > 0) && (downStrength >= strongThreshold); bool isSweepUp = false, isSweepDown = false; if(UseLiquiditySweepFilter) { double high = rates[1].high, low = rates[1].low, close = rates[1].close; isSweepUp = (high > upper1 + buf) && (close <= upper1 + buf); isSweepDown = (low < lower1 - buf) && (close >= lower1 - buf); } bool volExpanded = true; if(UseVolatilityExpansion && atrBuffer[0] > 0) { double atrNow = atrBuffer[0]; double atr10 = atrBuffer[10]; if(atr10 > 0) volExpanded = (atrNow / atr10) >= MinVolatilityExpansion; } bool structBullish = true, structBearish = true; if(UseStructuralBreak) { SwingPoint lastHigh, lastLow; GetLastSwing(highs,highCount,lows,lowCount,lastHigh,lastLow); if(crossedUp && lastHigh.time > 0) structBullish = (pUp1 > lastHigh.price); if(crossedDown && lastLow.time > 0) structBearish = (pLow1 < lastLow.price); } bool allowBullish = true, allowBearish = true; if(UseChannelTypeFilter) { if(ch.type == ASCENDING) allowBearish = false; else if(ch.type == DESCENDING) allowBullish = false; } //--- Bullish breakout handling if(crossedUp && strongUp && allowBullish && !isSweepUp && volExpanded && structBullish) { bool trigger = superStrongUp; if(!trigger) { trigger = true; for(int i=1; i<=BreakoutConfirmationBars; i++) { if(i >= ArraySize(rates)) { trigger = false; break; } datetime ti = rates[i].time; double ui = LinePriceAtTime(ch.high1,ch.high2,ti); double pi = BreakoutUseClose ? rates[i].close : rates[i].high; if(!(pi > ui + buf && (pi - (ui + buf)) >= minStrength)) { trigger = false; break; } } } if(trigger) { if(UseRetestMode) { // Store pending breakout if(pendingCount >= ArraySize(pendingBreakouts)) ArrayResize(pendingBreakouts,pendingCount+10); PendingBreakout pb; pb.breakoutTime = t1; pb.price = pUp1; pb.isBullish = true; pb.linePrice = upper1; pb.totalTouches = totalTouches; pb.channelSlope = ch.slope; pb.channelWidth = ch.width; pb.high1 = ch.high1; pb.high2 = ch.high2; pendingBreakouts[pendingCount++] = pb; if(DebugMode) Print("Bullish pending retest at ",TimeToString(t1)); } else { double arrowPrice = rates[1].low - currentATR*0.15; DrawPermanentArrow(t1,arrowPrice,true,totalTouches,rates[1].low,rates[1].high); } if(AlertOnBreakout) Alert(_Symbol," ",EnumToString(_Period),": BUY breakout"); } } //--- Bearish breakout handling (symmetrical) if(crossedDown && strongDown && allowBearish && !isSweepDown && volExpanded && structBearish) { bool trigger = superStrongDown; if(!trigger) { trigger = true; for(int i=1; i<=BreakoutConfirmationBars; i++) { if(i >= ArraySize(rates)) { trigger = false; break; } datetime ti = rates[i].time; double ui = LinePriceAtTime(ch.high1,ch.high2,ti); double li = ui - ch.width; double pi = BreakoutUseClose ? rates[i].close : rates[i].low; if(!(pi < li - buf && ((li - buf) - pi) >= minStrength)) { trigger = false; break; } } } if(trigger) { if(UseRetestMode) { if(pendingCount >= ArraySize(pendingBreakouts)) ArrayResize(pendingBreakouts,pendingCount+10); PendingBreakout pb; pb.breakoutTime = t1; pb.price = pLow1; pb.isBullish = false; pb.linePrice = lower1; pb.totalTouches = totalTouches; pb.channelSlope = ch.slope; pb.channelWidth = ch.width; pb.high1 = ch.high1; pb.high2 = ch.high2; pendingBreakouts[pendingCount++] = pb; if(DebugMode) Print("Bearish pending retest at ",TimeToString(t1)); } else { double arrowPrice = rates[1].high + currentATR*0.15; DrawPermanentArrow(t1,arrowPrice,false,totalTouches,rates[1].low,rates[1].high); } if(AlertOnBreakout) Alert(_Symbol," ",EnumToString(_Period),": SELL breakout"); } } } //+------------------------------------------------------------------+
Такая архитектура на основе состояний обеспечивает детерминированную и однократную обработку каждого пробоя, исключает дублирование сигналов и сохраняет прозрачность логики как в режиме немедленного подтверждения, так и в режиме ретеста.
Слой управления сигналами и сохранения состояния
Этот компонент обеспечивает надежное сохранение сигналов и визуальных меток при обновлении графика, повторной инициализации или перекомпиляции кода. Компонент поддерживает массивы, такие как breakoutsArray, где хранятся подтвержденные пробои вместе с такими данными, как время, цена и сила сигнала. Он отвечает за отрисовку постоянных стрелок и зон, отмечающих уровни пробоя и, если это задано в настройках, продлеваемых вправо на будущие бары (UseBreakoutZones). В режиме ретеста компонент постоянно отслеживает, возвращается ли цена в зону пробоя, и подтверждает сигнал только после успешного ретеста. Компонент управляет жизненным циклом графических объектов: на каждом тике удаляет временные объекты, сохраняя при этом видимость постоянных сигналов. Такой подход дает трейдеру последовательную визуальную среду и надежную опору для принятия решений, уменьшая путаницу после перезагрузки графика или изменений в коде.
//+------------------------------------------------------------------+ //| Store and draw confirmed signals | //+------------------------------------------------------------------+ void DrawPermanentArrow(datetime time,double price,bool isBullish,int totalTouches,double low=0,double high=0) { string arrowName = SIG_PREFIX + (isBullish ? "BUY_" : "SELL_") + IntegerToString((int)time) + "_" + IntegerToString(totalTouches); //--- Avoid duplicate arrows if(ObjectFind(0,arrowName) >= 0) return; if(!ObjectCreate(0,arrowName,OBJ_ARROW,0,time,price)) { if(DebugMode) Print("Failed to create arrow: ",GetLastError()); return; } ObjectSetInteger(0,arrowName,OBJPROP_ARROWCODE,isBullish ? 233 : 234); ObjectSetInteger(0,arrowName,OBJPROP_COLOR,isBullish ? BullArrowColor : BearArrowColor); ObjectSetInteger(0,arrowName,OBJPROP_WIDTH,ArrowSize); ObjectSetInteger(0,arrowName,OBJPROP_BACK,false); ObjectSetInteger(0,arrowName,OBJPROP_SELECTABLE,false); ObjectSetInteger(0,arrowName,OBJPROP_HIDDEN,true); string desc = "Channel Breakout " + (isBullish ? "BULLISH" : "BEARISH") + " (" + IntegerToString(totalTouches) + " touches)"; ObjectSetString(0,arrowName,OBJPROP_TEXT,desc); if(UseBreakoutZones && high > low) DrawBreakoutZone(time,low,high,isBullish,totalTouches); //--- Store in persistent array if(breakoutsCount >= ArraySize(breakoutsArray)) ArrayResize(breakoutsArray,breakoutsCount+10); breakoutsArray[breakoutsCount].time = time; breakoutsArray[breakoutsCount].price = price; breakoutsArray[breakoutsCount].isBullish = isBullish; breakoutsArray[breakoutsCount].totalTouches = totalTouches; breakoutsCount++; } //+------------------------------------------------------------------+
Перед сохранением или отрисовкой нового сигнала пробоя система проверяет, не был ли уже зафиксирован эквивалентный сигнал в том же структурном контексте. Это предотвращает повторную отрисовку стрелок во время затяжной волатильности или при нескольких тиках внутри одного бара. Каждый сохраненный пробой включает временную метку и структурную привязку, что позволяет системе соблюдать принцип "один сигнал на событие". Это обеспечивает ясность визуального представления и поддерживает надежность анализа.
Поддерживающие и служебные системы
Поддерживающие системы обеспечивают работу всех модулей, предоставляя базовые утилиты, такие как расчет ATR, управление объектами и вспомогательные математические функции. ATR обрабатывается через отдельный хэндл iATR, который обновляется на каждом баре и дает нормализованные показатели волатильности для фильтрации и построения зон во всей системе. Функции вроде LinePriceAtTime() вычисляют расчетную цену трендовой линии в заданный момент времени, а CountTouchesOnLine() определяет, сколько свингов относится к каждой границе канала, учитывая только точки на линии или рядом с ней в пределах заданного допуска и с учетом положения самой линии.
Функции классификации анализируют наклон каналов-кандидатов, относя их к восходящим, нисходящим или горизонтальным, что помогает и при фильтрации, и при формировании сигналов направленности. Функции градации силы касаний оценивают количество касаний, классифицируя каналы как сильные, средние или слабые, что помогает отсеивать менее надежные структуры. Вся система включает развернутую инфраструктуру отладки, которая фиксирует ключевые точки принятия решений, вычисления и результаты фильтров, облегчая разработчикам и трейдерам тонкую настройку параметров, разбор проблем и понимание логики работы.
//+------------------------------------------------------------------+ //| Calculate the slope between two swing points | //+------------------------------------------------------------------+ double CalculateSlope(const SwingPoint &a,const SwingPoint &b) { double deltaTime = (b.time - a.time); if(deltaTime == 0) return(0); return (b.price - a.price)/deltaTime; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Count touches on a trendline considering only swings on relevant side | //+------------------------------------------------------------------+ int CountTouchesOnLine(const SwingPoint &a,const SwingPoint &b, const SwingPoint &swings[],int swingCount, bool isUpperLine,double tolerance, int recentBars,int &recentCount) { int touches = 2; // the two anchor swings recentCount = 0; double timeDiff = (double)(b.time - a.time); if(timeDiff <= 0) return touches; double slope = (b.price - a.price) / timeDiff; for(int i=0; i<swingCount; i++) { if(swings[i].time == a.time || swings[i].time == b.time) continue; if(swings[i].time < a.time || swings[i].time > b.time) continue; double linePrice = a.price + slope * (swings[i].time - a.time); double distance = isUpperLine ? linePrice - swings[i].price : swings[i].price - linePrice; bool correctSide = (isUpperLine && distance >= -tolerance) || (!isUpperLine && distance >= -tolerance); if(correctSide && MathAbs(distance) <= tolerance) { touches++; if(swings[i].barIndex <= recentBars) recentCount++; } } return touches; } //+------------------------------------------------------------------+
Все этапы анализа работают с четко определенными структурами данных, такими как SwingPoint, Channel и BreakoutInfo. Такой структурированный подход закрепляет явное распределение данных и ответственности между модулями. Между функциями передаются не сырые ценовые значения, а инкапсулированные объекты, что обеспечивает целостность данных и снижает риск логических несоответствий. Такая архитектура упрощает сопровождение и позволяет в будущем расширять систему без перестройки архитектуры, например для отслеживания нескольких каналов или адаптации к нескольким символам.
Поток данных и взаимодействие модулей

Поток данных начинается с модуля выявления точек свинга, который анализирует недавние ценовые данные и находит значимые свинги – локальные максимумы и минимумы, определяющие структуру рынка. Эти точки свинга служат входными данными для модуля построения и оценки каналов, который перебором ищет наиболее статистически значимый параллельный канал, соответствующий этим свингам. Каждый кандидат оценивается по количеству касаний, давности и силе, после чего ему присваивается итоговый балл. Затем канал с наивысшим баллом передается в модуль визуализации, который отрисовывает трендовые линии с продлением вправо, а также добавляет информативные метки и маркеры.
Параллельно движок обнаружения и подтверждения пробоев проецирует эти трендовые линии на данные в реальном времени и постоянно отслеживает пересечения, которые могут указывать на пробой. Когда возникает потенциальный пробой, к нему применяются несколько фильтров – по волатильности, пробою структуры, ликвидности и расширению волатильности, – чтобы подтвердить событие. Только после прохождения всех фильтров система формирует торговый сигнал – либо сразу, либо после подтвержденного ретеста.
Слой управления сигналами и сохранения состояния обеспечивает надежное постоянное хранение этих сигналов вместе с их визуальными представлениями и их корректное отображение даже после перезагрузки графика. Он управляет жизненным циклом всех графических объектов: удаляет временные объекты на каждом тике и сохраняет постоянные сигналы и зоны. Это гарантирует, что у трейдера всегда будет ясная и актуальная визуальная подсказка и надежная история подтвержденных сигналов.
На всем протяжении этого процесса данные организуются через структурированные типы: от исходных точек свинга они переходят к вариантам каналов, а затем к подтвержденным сигналам, при этом каждый модуль отвечает за свой аспект анализа. Такой организованный поток данных поддерживает прозрачность системы, ее адаптивность и способность выдавать качественные подтвержденные сигналы пробоя.
Жизненный цикл исполнения и обработка событий
Советник работает в строгом соответствии с событийной моделью MetaTrader 5. Инициализация выполняется в OnInit(), где подготавливаются хэндлы индикаторов, например ATR, параметры конфигурации и префиксы имен графических объектов. На этом этапе выделяются все постоянные массивы, чтобы к началу обработки рыночных данных структура была полностью готова.
Основной цикл анализа выполняется внутри OnTick(). Однако для вычислительной эффективности структурные перерасчеты, такие как выявление свингов и перестроение канала, выполняются только при появлении нового бара. Это достигается за счет сохранения временной метки последнего обработанного бара и ее сравнения со временем открытия текущего бара (см. переменную static datetime lastBarTime во фрагменте OnTick() из подраздела о движке обнаружения и подтверждения пробоев). Запуск тяжелой логики только после проверки появления нового бара позволяет системе избегать лишней обработки на каждом тике и при этом сразу реагировать на условия пробоя.
Когда советник удаляют или перекомпилируют, OnDeinit() выполняет контролируемую очистку графических объектов и хэндлов индикаторов, чтобы на графике не оставалось лишних объектов. Такое дисциплинированное управление жизненным циклом повышает стабильность, эффективность и предсказуемость поведения при повторных инициализациях.
Тестирование и результаты
В этом разделе описан процесс тестирования, проведенного для оценки производительности и надежности инструмента. Тестирование проводилось по двум взаимодополняющим направлениям:
1) Тестирование в реальном времени на демо-графиках
Инструмент был развернут на демо-графике в условиях реального времени, чтобы оценить его отзывчивость, точность и стабильность на фоне текущих рыночных движений. Этот этап подтвердил способность системы выявлять реальные пробои, отфильтровывать ложные сигналы и выдавать наглядные визуальные сигналы в динамичной среде.
- Тестирование в реальном времени: EURUSD (H1)
На рисунке ниже показан классический медвежий пробой параллельного канала, зафиксированный системой на графике EURUSD H1 в середине февраля 2026 года, что демонстрирует эффективность модульной архитектуры в условиях, близких к реальным.

На графике показан явно выраженный нисходящий (медвежий) параллельный канал, сформировавшийся за несколько дней – примерно с 11 по 17 февраля 2026 года. Верхняя граница (зеленая линия) соединяет последние максимумы свинга, а нижняя идет параллельно ей и опирается на несколько минимумов свинга. К ключевым визуальным элементам, которые строит индикатор, относятся:
- точки свинга – отмечены красными кругами для максимумов и зелеными для минимумов, при этом используется минимальный размер свинга на основе ATR, чтобы отсекать несущественные колебания;
- линии канала – продолжены вправо с помощью LineExtensionBars, наглядно показывают нисходящий наклон и классифицируются функцией ClassifyChannel() как DESCENDING;
- индикаторы касаний – на обеих границах видны множественные касания, что обеспечивает высокий рейтинг канала и оценку силы MEDIUM/HIGH;
- сигнал пробоя – заметная красная стрелка вниз размещена рядом с закрытием свечи 18 февраля, в зоне около 1.1832-1.1820, и появляется только после того, как цена уверенно закрывается ниже нижней границы канала.
Этот пример показывает, как система объединяет выявление свингов, построение канала, подтверждение пробоя и постоянную визуальную индикацию, чтобы давать точную и практически применимую информацию в сценариях торговли в реальном времени.
2) Тестирование на исторических данных
Для оценки инструмента в разных рыночных условиях было проведено всестороннее тестирование на исторических данных по нескольким выборкам. Основной акцент делался на точности выявления корректных пробоев, доле ложных сигналов и таких метриках эффективности, как процент прибыльных сделок и соотношение риск/прибыль.
- Тестирование на исторических данных: EURUSD (H1)
Диаграмма ниже показывает результаты тестирования советника на исторических данных. Система последовательно выявляет значимые точки свинга и строит устойчивые параллельные каналы, одновременно отсекая незначительные колебания. И бычьи, и медвежьи пробои надежно фиксируются, а стрелки явно отмечают подтвержденные сигналы.

В целом результаты тестирования на исторических данных подтверждают, что советник сохраняет структурную точность, корректно подтверждает пробои и дает наглядные визуальные ориентиры в разных исторических рыночных сценариях, что подчеркивает надежность и устойчивость инструмента.
Заключение
Модульная архитектура этой системы демонстрирует комплексный подход к выявлению, подтверждению и визуализации пробоев параллельных каналов на языке MQL5. Благодаря тщательно выстроенным компонентам – выявлению свингов, построению каналов, подтверждению пробоя, визуальной сигнализации и адаптивному перерасчету – инструмент сохраняет и точность, и отзывчивость в условиях рынка в реальном времени.
Тестирование в реальном времени на демо-графиках подтвердило способность системы фиксировать значимые пробои, отсеивать шум и давать ясные, пригодные для работы визуальные сигналы. Дополнительное тестирование на исторических данных по нескольким выборкам подтвердило надежность советника и показало стабильные результаты в выявлении значимых точек свинга, построении качественных каналов и надежной сигнализации как бычьих, так и медвежьих пробоев.
В совокупности эти результаты дают трейдеру надежную основу для точного и структурно выверенного анализа пробоев. Сочетание адаптивности в реальном времени, подтвержденной исторической надежности и постоянной визуальной обратной связи позволяет пользователям уверенно интерпретировать рыночное поведение и действовать на основе проверенных сигналов, что делает систему универсальным и практичным инструментом для профессионального анализа графиков на языке MQL5.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21443
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Знакомство с языком MQL5 (Часть 42): Руководство для начинающих по работе с файлами в MQL5 (IV)
Моделирование рынка: Первые шаги на SQL в MQL5 (IV)
TradeMux как Quant Backbone: Подключение институциональных Python-пайплайнов к разным терминалам и брокерам
Повышение эффективности торговли с использованием Smart Money Concepts (SMC): OB, BOS и FVG
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования