English
preview
От новичка до эксперта: Выявление переворотов зон ликвидности на языке MQL5

От новичка до эксперта: Выявление переворотов зон ликвидности на языке MQL5

MetaTrader 5 — Примеры |
58 0
Clemence Benjamin
Clemence Benjamin

Содержание


Введение

Каждый трейдер, который ориентируется на уровни поддержки и сопротивления, рано или поздно сталкивается с одной и той же проблемой. Ценовой уровень, который неоднократно выступал сопротивлением — что часто подтверждалось длинными верхними тенями или свечами медвежьего поглощения, — внезапно пробивается сильным бычьим движением. Интуитивно можно ожидать, что цена продолжит расти, подтвердив пробой. Однако рынок часто ведет себя иначе: цена возвращается к тому же уровню, который теперь выступает поддержкой, и отскакивает вверх, словно уровень сменил свою роль.

То, что когда-то служило препятствием, теперь, похоже, притягивает цену. Для многих трейдеров, которые рассматривают уровни поддержки и сопротивления как неизменные объекты на графике, такое поведение может вызывать недоумение. На самом деле это явление хорошо известно в техническом анализе и обычно называется сменой роли (переворотом зоны). Ознакомьтесь с приведенными ниже иллюстрациями. На рис. 1 показана зона спроса (поддержка), которую индикатор выделил зеленым цветом, обозначающим поддержку. На рис. 2, напротив, видно, что после пробоя вниз зона начинает выступать сопротивлением. В этот момент система автоматически меняет роль зоны и ее цвет на красный.

Before liquidity flip

Рис. 1. До переворота зоны ликвидности

After liquidity flip

Рис. 2. После переворота зоны ликвидности

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

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

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

Когда достаточно сильное бычье движение пробивает зону предложения, индикатор автоматически преобразует ее в зону спроса. Цвет прямоугольника меняется, срок его действия продлевается, и по зоне вновь могут формироваться сигналы реакции. Та же логика применяется в обратном направлении, когда зона спроса пробивается сильным медвежьим импульсом.

В результате получается динамическая визуализация структуры рынка — живая карта спроса и предложения, которая меняется вместе с ценой.

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


Концепция

Идея о том, что пробитый уровень может изменить свою роль, не нова. Техническим аналитикам она известна уже почти столетие.

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

Эти действия естественным образом укрепляют уровень в его новой роли.

Позже Джон Дж. Мерфи формализовал эту концепцию в получившей широкое признание книге "Технический анализ финансовых рынков". Сегодня этот принцип — одна из основ современного технического анализа, хотя его программная реализация по-прежнему остается сложной задачей.

С количественной точки зрения зону ликвидности можно задать двумя ценовыми границами:

  • верхняя граница — максимальная цена зоны;
  • нижняя граница — минимальная цена зоны.

Переворот зоны происходит, когда импульсная свеча уверенно закрывается за пределами этих границ, отражая реальную активность участников рынка, а не случайную волатильность.

Чтобы задать объективные критерии "уверенного" пробоя, мы одновременно применяем два условия:

1. Подтвержденный пробой.

Цена должна закрыться за пределами зоны:

  • выше верхней границы — для зон предложения;
  • ниже нижней границы — для зон спроса.

Это требование позволяет исключить кратковременный выход цены за границу зоны, после которого она возвращается обратно до закрытия свечи.

2. Условие силы импульса.

Диапазон свечи (High − Low) должен превышать высоту зоны, умноженную на заданный коэффициент.

На практике этот множитель обычно находится в диапазоне от 1,5 до 2,0 и может настраиваться пользователем.

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

Зоны ликвидности приобретают еще большее значение, когда они формируются на старших таймфреймах (HTF). Эти уровни часто соответствуют областям, где институциональные трейдеры, банки и крупные фонды размещали крупные ордера.

Например, анализ зон, обнаруженных на таймфрейме H1, при торговле на графике M15 помогает отфильтровать шум младших таймфреймов и сосредоточить внимание на структурно важных уровнях.

Наш индикатор обнаруживает эти зоны по паттерну "база → импульс" на исторических данных. Паттерн состоит из двух элементов:

  • базовая свеча — относительно небольшая свеча, отражающая краткий период консолидации;
  • импульсная свеча — крупная свеча, которая следует сразу за базовой и сигнализирует о сильном направленном движении.

Границы зоны задаются полным диапазоном базовой свечи. Импульсная свеча подтверждает, что на этом уровне существовал значительный дисбаланс.

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

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

Главная отличительная особенность индикатора — механизм переворота зон в реальном времени.

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

Когда импульсный пробой удовлетворяет двум ранее заданным условиям, индикатор немедленно выполняет следующие действия:

  • Исходный прямоугольник удаляется.
  • Отображается новый прямоугольник, обозначающий зону с противоположной ролью.
  • Цвет зоны меняется в соответствии с ее новым типом:
    • лаймово-зеленый — для зоны спроса;
    • томатно-красный — для зоны предложения.
  • Срок действия зоны продлевается с отсчетом от момента пробоя.

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

Благодаря продлению срока действия зоны после переворота остаются видимыми достаточно долго, чтобы трейдеры могли наблюдать возможные ретесты и торговать на реакции цены от нового уровня.

В результате получается динамическая система отображения поддержки и сопротивления, которая обновляется в реальном времени и отражает реальное поведение ликвидности на современных финансовых рынках.


Реализация

Мы будем пошагово разрабатывать этот индикатор. Каждый компонент описан достаточно подробно для начинающих программистов на языке MQL5; при этом для опытных разработчиков выделены ключевые архитектурные решения. Полный исходный код приведен в конце этого раздела. Сначала ознакомьтесь с пояснениями, чтобы понять, как все компоненты взаимосвязаны.

Обзор структуры индикатора

Индикатор можно установить на любой выбранный пользователем график с любым таймфреймом. Он хранит внутренний динамический массив структур LiquidityZone, каждая из которых описывает активную в данный момент зону со всеми ее свойствами. На каждом тике вызывается функция OnCalculate, которая выполняет три группы задач в заданной последовательности.

Сначала проверяется, появился ли новый бар на выбранном пользователем старшем таймфрейме. Если на старшем таймфрейме появился новый бар, вызывается функция UpdateZones для поиска в его данных новых паттернов "база → импульс", соответствующих критериям допустимых зон.

Затем она проверяет наличие сигналов реакции: вошла ли цена в какую-либо активную зону и сформировался ли распознаваемый разворотный паттерн — свеча поглощения, пин-бар или пробой внутреннего бара. При появлении такого сигнала на графике отображается стрелка, а если алерты включены, трейдер получает уведомление.

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

Входные параметры

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

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

Группа параметров сигналов реакции управляет внешним видом и поведением стрелок входа, когда цена реагирует на зону с формированием разворотного паттерна. Группа параметров переворота зон включает (1) булев переключатель, разрешающий или запрещающий переворот, и (2) множитель, задающий минимальную силу импульса пробоя, необходимую для переворота.

Такое решение дает трейдерам полную свободу настройки индикатора под свой стиль торговли и особенности используемых инструментов.

//--- Inputs for zone detection (higher timeframe)
input ENUM_TIMEFRAMES ZoneTimeframe   = PERIOD_H1;      // Timeframe for zone detection
input int             LookbackBars    = 1000;          // Number of bars on higher TF to look back
input double          RatioMultiplier = 3.0;           // Ratio Multiplier for zone formation
input int             ExtendBars      = 50;            // Bars to extend rectangle right (on higher TF)
input bool            FillRectangles  = true;         // Fill or just border?
input color           DemandZoneColor = clrLimeGreen;  // Demand zone rectangle color
input color           SupplyZoneColor = clrTomato;     // Supply zone rectangle color
input uchar           ZoneOpacity     = 40;            // 0-255 transparency

//--- Inputs for reaction signals (current timeframe)
input color           DemandArrowColor = clrLimeGreen; // Buy arrow color
input color           SupplyArrowColor = clrRed;        // Sell arrow color
input int             ArrowSize        = 1;            // Arrow width

//--- Inputs for zone flipping (new intelligence)
input bool            EnableZoneFlipping = true;      // Enable flipping zones after impulsive violation
input double          ViolationMultiplier = 1.5;      // Multiplier for impulsive violation (zone height)

Структура зоны и глобальные переменные

В основе управления данными индикатора лежит структура LiquidityZone, которая инкапсулирует всю информацию, необходимую для представления отдельной зоны, ее отрисовки и управления ею на протяжении всего жизненного цикла. Эта структура содержит значения с плавающей запятой двойной точности для верхней и нижней ценовых границ, определяющих высоту отображаемого прямоугольника. Она содержит поля типа datetime для времени начала (времени открытия бара, сформировавшего зону) и времени истечения срока действия. Последнее рассчитывается как время начала плюс число баров, на которое продлевается зона, умноженное на длительность бара старшего таймфрейма в секундах.

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

//+------------------------------------------------------------------+
//| Structure representing a liquidity zone                          |
//+------------------------------------------------------------------+
struct LiquidityZone
  {
   double   high;          //--- zone top price
   double   low;           //--- zone bottom price
   datetime start_time;    //--- bar time where zone was formed (or flipped)
   datetime expiry_time;   //--- time when zone expires (start + ExtendBars * htfPeriod)
   bool     demand;        //--- true = demand, false = supply
   bool     triggered;     //--- true if a signal already fired in this zone
  };

//--- Dynamic array storing currently active liquidity zones
LiquidityZone zones[];

Помимо массива зон, работу индикатора обеспечивают несколько важных глобальных переменных. Переменная myPoint хранит значение пункта, скорректированное для брокеров с пятизначными и трехзначными котировками, что обеспечивает единообразное расположение стрелок для разных типов символов. Переменные htfPeriodSeconds и currentPeriodSeconds хранят длительность периодов в секундах для временных расчетов, позволяя точно управлять сроком действия зон. Переменная lastHtfUpdateTime хранит время последнего обновления зон, позволяя избежать ненужных пересчетов, а lastAlertTime ограничивает частоту алертов, чтобы один и тот же сигнал не вызывал их повторно.

Инициализация OnInit

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

Сначала функция получает значение пункта текущего символа и умножает его на 10 для брокеров с котировками, учитывающими дробные доли пипса. Благодаря этому стрелки размещаются на удобном для восприятия расстоянии от ценовых баров. Затем с помощью функции SetIndexBuffer с флагом INDICATOR_DATA она настраивает два буфера индикатора: для стрелок покупки и для стрелок продажи. Для обоих буферов с помощью ArraySetAsSeries со значением true устанавливается индексация как у временных рядов: индекс 0 соответствует последнему бару. Это упрощает последующие вычисления.

Функции PlotIndexSetInteger и PlotIndexSetString задают стиль отрисовки, коды стрелок, метки, цвета и ширину для обоих типов стрелок. Наконец, функция вычисляет длительность текущего и старшего таймфреймов в секундах, используя резервное значение в один час, если функция PeriodSeconds возвращает недопустимое значение. Метке времени обновления присваивается 0, чтобы при первом вызове OnCalculate выполнить первоначальный поиск зон.

//+------------------------------------------------------------------+
//| Initializes the indicator and configures buffers and plots       |
//+------------------------------------------------------------------+
int OnInit()
  {
   myPoint = Point();
   if(_Digits == 5 || _Digits == 3)
      myPoint *= 10;

   //--- Set arrow buffers
   SetIndexBuffer(0, BuyArrow, INDICATOR_DATA);
   SetIndexBuffer(1, SellArrow, INDICATOR_DATA);
   ArraySetAsSeries(BuyArrow, true);
   ArraySetAsSeries(SellArrow, true);

   PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW);
   PlotIndexSetInteger(0, PLOT_ARROW, 233);      //--- up arrow
   PlotIndexSetString(0, PLOT_LABEL, "Buy");
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, DemandArrowColor);
   PlotIndexSetInteger(0, PLOT_LINE_WIDTH, ArrowSize);

   PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_ARROW);
   PlotIndexSetInteger(1, PLOT_ARROW, 234);      //--- down arrow
   PlotIndexSetString(1, PLOT_LABEL, "Sell");
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, SupplyArrowColor);
   PlotIndexSetInteger(1, PLOT_LINE_WIDTH, ArrowSize);

   ArrayInitialize(BuyArrow, EMPTY_VALUE);
   ArrayInitialize(SellArrow, EMPTY_VALUE);

   //--- Period seconds for time calculations
   currentPeriodSeconds = PeriodSeconds();
   htfPeriodSeconds     = PeriodSeconds(ZoneTimeframe);

   if(htfPeriodSeconds <= 0)
      htfPeriodSeconds = 3600;   //--- fallback to 1 hour

   lastHtfUpdateTime = 0;

   return(INIT_SUCCEEDED);
  }

Деинициализация и вспомогательные функции отрисовки

Корректное удаление созданных объектов необходимо для любого индикатора, работающего с графическими объектами: лишние прямоугольники могут накапливаться на графиках и вызывать путаницу. Функция OnDeinit автоматически вызывается при удалении индикатора и запускает DeleteAllZones для удаления созданных объектов. Функция DeleteAllZones перебирает все объекты на графике, используя ObjectsTotal с параметрами, задающими текущий график, все типы объектов и все подокна.

Для каждого объекта она получает имя с помощью ObjectName и проверяет с помощью StringFind, начинается ли оно с префикса LiqZone_. Если найдено совпадение, объект удаляется с помощью ObjectDelete. После завершения цикла ChartRedraw немедленно обновляет график, отображая результат удаления объектов.

//+------------------------------------------------------------------+
//| Deletes all liquidity zone rectangles from the chart             |
//+------------------------------------------------------------------+
void DeleteAllZones()
  {
   int total = ObjectsTotal(0, 0, -1);

   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);

      if(StringFind(name, "LiqZone_") == 0)
         ObjectDelete(0, name);
     }

   ChartRedraw();
  }
//+------------------------------------------------------------------+

Функция DrawZone инкапсулирует всю логику создания и оформления прямоугольников зон. Сначала формируется уникальное имя объекта на основе времени начала и времени истечения срока действия, чтобы зоны с разными временными диапазонами не конфликтовали. Если объект с таким именем уже существует, он удаляется, чтобы избежать дублирования. Цвет зоны зависит от параметра isDemand: в соответствии с ним из входных настроек выбирается цвет зоны спроса или предложения.

ObjectCreate создает объект-прямоугольник с заданными временными и ценовыми координатами, а вызовы ObjectSetInteger настраивают его свойства: цвет обводки, сплошной стиль линии, толщину линии, заливку, размещение на заднем плане и возможность выделения. Если заливка включена, ColorToARGB объединяет цвет зоны с заданным пользователем значением непрозрачности. Полученный цвет используется для полупрозрачной заливки фона. В результате зоны выглядят привлекательно и не скрывают полностью расположенные за ними ценовые свечи.

//+------------------------------------------------------------------+
//| Draws a liquidity zone rectangle on the chart                    |
//+------------------------------------------------------------------+
void DrawZone(datetime start_time,
              datetime expiry_time,
              double   price_top,
              double   price_bottom,
              const bool isDemand)
  {
   string obj_name = "LiqZone_" + IntegerToString(start_time) + "_" + IntegerToString(expiry_time);

   if(ObjectFind(0, obj_name) >= 0)
      ObjectDelete(0, obj_name);

   color zone_color = isDemand ? DemandZoneColor : SupplyZoneColor;

   if(ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0,
                   start_time, price_top, expiry_time, price_bottom))
     {
      ObjectSetInteger(0, obj_name, OBJPROP_COLOR, zone_color);
      ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
      ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, obj_name, OBJPROP_FILL, FillRectangles);
      ObjectSetInteger(0, obj_name, OBJPROP_BACK, true);       //--- behind price
      ObjectSetInteger(0, obj_name, OBJPROP_SELECTABLE, false);

      if(FillRectangles)
        {
         color fill_clr = (color)ColorToARGB(zone_color, ZoneOpacity);
         ObjectSetInteger(0, obj_name, OBJPROP_BGCOLOR, fill_clr);
        }
     }
  }
//+------------------------------------------------------------------+

Обновление зон по данным старшего таймфрейма

Функция UpdateZones — основа механизма обнаружения зон в индикаторе. Она просматривает данные старшего таймфрейма и выявляет новые зоны ликвидности по паттерну "база → импульс". Функция начинает работу с копирования необходимых данных со старшего таймфрейма с помощью функций CopyOpen, CopyClose, CopyHigh, CopyLow и CopyTime. Запрашивается количество баров, заданное LookbackBars: это обеспечивает достаточный объем исторических данных для обнаружения зон без чрезмерного использования памяти.

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

//+------------------------------------------------------------------+
//| Updates liquidity zones using higher timeframe structure         |
//+------------------------------------------------------------------+
void UpdateZones()
  {
   double   htf_open[], htf_close[], htf_high[], htf_low[];
   datetime htf_time[];

   int htf_bars = CopyOpen(Symbol(), ZoneTimeframe, 0, LookbackBars, htf_open);

   if(htf_bars <= 0)
      return;

   CopyClose(Symbol(), ZoneTimeframe, 0, LookbackBars, htf_close);
   CopyHigh(Symbol(),  ZoneTimeframe, 0, LookbackBars, htf_high);
   CopyLow(Symbol(),   ZoneTimeframe, 0, LookbackBars, htf_low);
   CopyTime(Symbol(),  ZoneTimeframe, 0, LookbackBars, htf_time);

   ArraySetAsSeries(htf_open,  true);
   ArraySetAsSeries(htf_close, true);
   ArraySetAsSeries(htf_high,  true);
   ArraySetAsSeries(htf_low,   true);
   ArraySetAsSeries(htf_time,  true);

   for(int i = htf_bars - 5; i >= 2; i--)
     {
      int base_idx     = i + 2;
      int impulse_idx = i + 1;

      if(base_idx >= htf_bars)
         continue;

      double base_range     = htf_high[base_idx] - htf_low[base_idx];
      double impulse_range = htf_high[impulse_idx] - htf_low[impulse_idx];

      if(base_range <= 0)
         continue;

      //--- Demand zone condition (both bars bullish, impulse expands)
      if(htf_open[base_idx] < htf_close[base_idx] &&
         htf_open[impulse_idx] < htf_close[impulse_idx] &&
         impulse_range >= base_range * RatioMultiplier)
        {
         bool exists = false;

         for(int j = 0; j < ArraySize(zones); j++)
           {
            if(zones[j].start_time == htf_time[base_idx])
              {
               exists = true;
               break;
              }
           }

         if(!exists)
           {
            LiquidityZone z;

            z.high        = htf_high[base_idx];
            z.low         = htf_low[base_idx];
            z.start_time  = htf_time[base_idx];
            z.expiry_time = z.start_time + ExtendBars * htfPeriodSeconds;
            z.demand      = true;
            z.triggered   = false;

            int size = ArraySize(zones);
            ArrayResize(zones, size + 1);
            zones[size] = z;

            DrawZone(z.start_time, z.expiry_time, z.high, z.low, true);
           }
        }

      //--- Supply zone condition (both bars bearish, impulse expands)
      if(htf_open[base_idx] > htf_close[base_idx] &&
         htf_open[impulse_idx] > htf_close[impulse_idx] &&
         impulse_range >= base_range * RatioMultiplier)
        {
         bool exists = false;

         for(int j = 0; j < ArraySize(zones); j++)
           {
            if(zones[j].start_time == htf_time[base_idx])
              {
               exists = true;
               break;
              }
           }

         if(!exists)
           {
            LiquidityZone z;

            z.high        = htf_high[base_idx];
            z.low         = htf_low[base_idx];
            z.start_time  = htf_time[base_idx];
            z.expiry_time = z.start_time + ExtendBars * htfPeriodSeconds;
            z.demand      = false;
            z.triggered   = false;

            int size = ArraySize(zones);
            ArrayResize(zones, size + 1);
            zones[size] = z;

            DrawZone(z.start_time, z.expiry_time, z.high, z.low, false);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Цикл поиска начинается с индекса (htf_bars − 5) и продолжается с уменьшением индекса до 2. Начальное смещение обеспечивает запас баров для обращения по индексам базового и импульсного баров. На каждой итерации мы вычисляем индекс базового бара как i плюс 2, а индекс импульсного бара — как i плюс 1. Такая схема смещений позволяет искать базовый бар, за которым сразу следует импульсный, — классическую формацию спроса и предложения. Убедившись, что индекс базового бара находится в границах массива, мы вычисляем диапазоны обоих баров и пропускаем бары с нулевым или отрицательным диапазоном, который указывал бы на некорректные данные.

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

Если дубликата нет, мы создаем новую структуру LiquidityZone, заполняем все ее поля, включая время истечения срока действия, рассчитанное на основе времени начала, и добавляем ее в массив zones, изменяя его размер с помощью ArrayResize. Наконец, мы вызываем DrawZone, чтобы создать на графике прямоугольник нужного для зоны спроса цвета. Обнаружение зоны предложения выполняется по той же схеме, но оба бара должны быть медвежьими.

Удаление зон с истекшим сроком действия

Структура рынка меняется, и зоны, актуальные в прошлом, со временем теряют свое значение. Функция RemoveExpiredZones решает эту задачу, периодически удаляя зоны с истекшим сроком действия. Сначала с помощью TimeCurrent получается текущее серверное время. Благодаря этому срок действия зон определяется в той же системе времени, которую брокер использует для временных меток баров. Затем функция проходит массив zones в обратном порядке, уменьшая индекс. Это необходимо, поскольку при удалении элементов остальные элементы будут сдвигаться.

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

//+------------------------------------------------------------------+
//| Removes expired liquidity zones and deletes their chart objects  |
//+------------------------------------------------------------------+
void RemoveExpiredZones()
  {
   datetime current_time = TimeCurrent();

   for(int i = ArraySize(zones) - 1; i >= 0; i--)
     {
      if(current_time > zones[i].expiry_time)
        {
         string obj_name = "LiqZone_" + IntegerToString(zones[i].start_time) + "_" + IntegerToString(zones[i].expiry_time);

         ObjectDelete(0, obj_name);

         for(int j = i; j < ArraySize(zones) - 1; j++)
            zones[j] = zones[j + 1];

         ArrayResize(zones, ArraySize(zones) - 1);
        }
     }
  }
//+------------------------------------------------------------------+

Переворот зоны

Функция FlipZone реализует ключевую логику, которая отличает этот индикатор от традиционных инструментов для работы с зонами. Эта функция принимает индекс зоны, булево значение, задающее ее новый тип, и время пробоя, а затем полностью преобразует существующую зону с учетом новой рыночной ситуации. Сначала проверяется, находится ли индекс зоны в текущих границах массива. Затем функция формирует имя существующего объекта-прямоугольника и удаляет его, убирая с графика прежнее отображение зоны. Флагу, указывающему, является ли зона зоной спроса, присваивается новое значение. При последующей отрисовке это изменит цвет зоны.

Новое время истечения срока действия рассчитывается как время пробоя плюс произведение того же значения ExtendBars и длительности старшего таймфрейма в секундах. Таким образом, после переворота зона получает новый срок действия, равный сроку действия вновь сформированных зон. Флаг срабатывания сбрасывается в false, позволяя формировать новые сигналы реакции по зоне после изменения ее роли. Наконец, DrawZone вызывается с новым значением признака зоны спроса и создает прямоугольник другого цвета, наглядно показывающий трейдеру смену роли зоны. Обратите внимание, что мы намеренно сохраняем исходное значение start_time: функция обновления зон по данным старшего таймфрейма не создаст исходную зону повторно, поскольку в массиве zones уже есть зона с таким start_time.

//+------------------------------------------------------------------+
//| Flips an existing liquidity zone after a structural violation    |
//+------------------------------------------------------------------+
void FlipZone(const int zoneIndex,
              const bool newDemand,
              const datetime violation_time)
  {
   if(zoneIndex < 0 || zoneIndex >= ArraySize(zones))
      return;

   string obj_name = "LiqZone_" + IntegerToString(zones[zoneIndex].start_time) + "_" + IntegerToString(zones[zoneIndex].expiry_time);

   ObjectDelete(0, obj_name);

   zones[zoneIndex].demand     = newDemand;
   zones[zoneIndex].expiry_time = violation_time + ExtendBars * htfPeriodSeconds;
   zones[zoneIndex].triggered   = false;

   DrawZone(zones[zoneIndex].start_time,
            zones[zoneIndex].expiry_time,
            zones[zoneIndex].high,
            zones[zoneIndex].low,
            newDemand);
  }
//+------------------------------------------------------------------+

Функции распознавания разворотных паттернов

Чтобы формировать сигналы для входа при реакции цены на зону, индикатор использует комплексный набор функций распознавания свечных паттернов. Эти функции работают с массивами данных текущего таймфрейма Open, Close, High и Low, обновляемыми в OnCalculate. Функция BullishEngulfing выявляет паттерн из двух баров: текущий бар — бычий, а его тело поглощает тело предыдущего медвежьего бара. Функция BearishEngulfing выявляет противоположный паттерн. Функция BullishPinBar распознает свечи с длинной нижней тенью, как минимум вдвое превышающей размер тела. Это указывает на отбой от более низких цен и возможное давление покупателей.

Функция BearishPinBar выявляет свечи с длинной верхней тенью, что указывает на отбой от более высоких цен. Функция BullishInsideBreak обнаруживает паттерн консолидации, при котором цена пробивает вверх максимум предыдущего внутреннего бара, а BearishInsideBreak обнаруживает пробой вниз его минимума. Результаты отдельных проверок паттернов объединяются в DemandReversal и SupplyReversal, которые возвращают true при наличии любого из соответствующих бычьих или медвежьих паттернов. Благодаря такой модульной архитектуре в будущем можно будет легко добавлять новые типы паттернов, не нарушая существующую логику.

//+------------------------------------------------------------------+
//| Checks for a bullish engulfing candlestick pattern               |
//+------------------------------------------------------------------+
bool BullishEngulfing(const int i)
  {
   if(Close[i] > Open[i] &&
      Close[i+1] < Open[i+1] &&
      Close[i] > Open[i+1] &&
      Open[i] < Close[i+1])
      return(true);

   return(false);
  }

//+------------------------------------------------------------------+
//| Checks for a bearish engulfing candlestick pattern               |
//+------------------------------------------------------------------+
bool BearishEngulfing(const int i)
  {
   if(Close[i] < Open[i] &&
      Close[i+1] > Open[i+1] &&
      Open[i] > Close[i+1] &&
      Close[i] < Open[i+1])
      return(true);

   return(false);
  }

//+------------------------------------------------------------------+
//| Checks for a bullish pin bar reversal pattern                    |
//+------------------------------------------------------------------+
bool BullishPinBar(const int i)
  {
   double body  = MathAbs(Close[i] - Open[i]);
   double lower = MathMin(Open[i], Close[i]) - Low[i];

   if(lower > body * 2)
      return(true);

   return(false);
  }

//+------------------------------------------------------------------+
//| Checks for a bearish pin bar reversal pattern                    |
//+------------------------------------------------------------------+
bool BearishPinBar(const int i)
  {
   double body  = MathAbs(Close[i] - Open[i]);
   double upper = High[i] - MathMax(Open[i], Close[i]);

   if(upper > body * 2)
      return(true);

   return(false);
  }

//+------------------------------------------------------------------+
//| Detects bullish breakout from an inside bar structure            |
//+------------------------------------------------------------------+
bool BullishInsideBreak(const int i)
  {
   if(High[i] < High[i+1] && Low[i] > Low[i+1])
     {
      if(Close[i] > High[i+1])
         return(true);
     }

   return(false);
  }

//+------------------------------------------------------------------+
//| Detects bearish breakout from an inside bar structure            |
//+------------------------------------------------------------------+
bool BearishInsideBreak(const int i)
  {
   if(High[i] < High[i+1] && Low[i] > Low[i+1])
     {
      if(Close[i] < Low[i+1])
         return(true);
     }

   return(false);
  }

//+------------------------------------------------------------------+
//| Confirms bullish reversal conditions inside a demand zone        |
//+------------------------------------------------------------------+
bool DemandReversal(const int i)
  {
   if(BullishEngulfing(i))
      return(true);

   if(BullishPinBar(i))
      return(true);

   if(BullishInsideBreak(i))
      return(true);

   return(false);
  }

//+------------------------------------------------------------------+
//| Confirms bearish reversal conditions inside a supply zone        |
//+------------------------------------------------------------------+
bool SupplyReversal(const int i)
  {
   if(BearishEngulfing(i))
      return(true);

   if(BearishPinBar(i))
      return(true);

   if(BearishInsideBreak(i))
      return(true);

   return(false);
  }
//+------------------------------------------------------------------+

OnCalculate — объединение всех элементов

OnCalculate вызывается при каждом тике. Эта функция координирует работу остальных функций в заданной последовательности. Сначала выполняется защитная проверка: если rates_total меньше 5, баров недостаточно для надежного обнаружения паттернов, поэтому функция сразу возвращает 0. Далее мы копируем данные текущего таймфрейма в глобальные массивы с помощью CopyOpen, CopyClose, CopyHigh, CopyLow и CopyTime. Мы запрашиваем до 5 000 баров, но не больше rates_total, что обеспечивает достаточный объем исторических данных без чрезмерного использования памяти. После копирования мы с помощью ArraySetAsSeries устанавливаем для всех массивов индексацию как у временных рядов: индекс 0 соответствует последнему бару, что упрощает последующие обращения по индексам.

//+------------------------------------------------------------------+
//| Calculates indicator values and manages liquidity zone reactions |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   //--- Require minimum bars
   if(rates_total < 5)
      return(0);

   int barsNeeded = MathMin(rates_total, 5000);

   CopyOpen(Symbol(), PERIOD_CURRENT, 0, barsNeeded, Open);
   CopyClose(Symbol(), PERIOD_CURRENT, 0, barsNeeded, Close);
   CopyHigh(Symbol(), PERIOD_CURRENT, 0, barsNeeded, High);
   CopyLow(Symbol(), PERIOD_CURRENT, 0, barsNeeded, Low);
   CopyTime(Symbol(), PERIOD_CURRENT, 0, barsNeeded, TimeArray);

   ArraySetAsSeries(Open,       true);
   ArraySetAsSeries(Close,      true);
   ArraySetAsSeries(High,       true);
   ArraySetAsSeries(Low,        true);
   ArraySetAsSeries(TimeArray, true);

   //--- Update higher timeframe zones if new bar
   datetime latestHtfTime = iTime(Symbol(), ZoneTimeframe, 0);
   if(latestHtfTime != lastHtfUpdateTime)
     {
      UpdateZones();
      lastHtfUpdateTime = latestHtfTime;
     }

   RemoveExpiredZones();

   int signal_bar = 1;
   if(signal_bar >= rates_total)
      return(rates_total);

   //--- First, check for reversal signals
   for(int z = 0; z < ArraySize(zones); z++)
     {
      if(TimeCurrent() > zones[z].expiry_time)
         continue;

      if(zones[z].triggered)
         continue;

      if(PriceInsideZone(signal_bar, zones[z], High, Low))
        {
         if(zones[z].demand)
           {
            if(DemandReversal(signal_bar))
              {
               BuyArrow[signal_bar] = Low[signal_bar] - 5 * myPoint;
               zones[z].triggered   = true;

               if(TimeArray[signal_bar] != lastAlertTime)
                 {
                  Alert("Demand Zone BUY Reaction ", Symbol(), " ", EnumToString(_Period));
                  lastAlertTime = TimeArray[signal_bar];
                 }
              }
           }
         else
           {
            if(SupplyReversal(signal_bar))
              {
               SellArrow[signal_bar] = High[signal_bar] + 5 * myPoint;
               zones[z].triggered     = true;

               if(TimeArray[signal_bar] != lastAlertTime)
                 {
                  Alert("Supply Zone SELL Reaction ", Symbol(), " ", EnumToString(_Period));
                  lastAlertTime = TimeArray[signal_bar];
                 }
              }
           }
        }
     }

   //--- Second, check for impulsive violations (zone flipping)
   if(EnableZoneFlipping)
     {
      for(int z = 0; z < ArraySize(zones); z++)
        {
         if(TimeCurrent() > zones[z].expiry_time)
            continue;

         double zoneHeight = zones[z].high - zones[z].low;
         double barRange   = High[signal_bar] - Low[signal_bar];

         if(!zones[z].demand)   //--- supply zone
           {
            if(Close[signal_bar] > zones[z].high &&
               Close[signal_bar] > Open[signal_bar] &&
               barRange >= zoneHeight * ViolationMultiplier)
              {
               FlipZone(z, true, TimeArray[signal_bar]);
              }
           }
         else  //--- demand zone
           {
            if(Close[signal_bar] < zones[z].low &&
               Close[signal_bar] < Open[signal_bar] &&
               barRange >= zoneHeight * ViolationMultiplier)
              {
               FlipZone(z, false, TimeArray[signal_bar]);
              }
           }
        }
     }

   return(rates_total);
  }
//+------------------------------------------------------------------+

После подготовки массивов данных OnCalculate проверяет, появился ли новый бар на старшем таймфрейме: вызывает iTime для таймфрейма зон и сравнивает полученное значение с сохраненным lastHtfUpdateTime. Если значения различаются, значит, появился новый бар. Вызывается UpdateZones для поиска новых зон, после чего обновляется lastHtfUpdateTime. Затем RemoveExpiredZones удаляет зоны с истекшим сроком действия, поддерживая массив zones в актуальном состоянии и не допуская загромождения графика.

Затем индикатор присваивает signal_bar значение 1, соответствующее последнему закрытому бару. Это стандартный выбор для сигналов торговой системы, поскольку он позволяет избежать перерисовки и ошибок из-за заглядывания в будущее. Если индекс signal_bar выходит за границы доступного диапазона баров, функция возвращает rates_total без дальнейшей обработки. Цикл обработки сигналов разворота перебирает все активные зоны, пропуская зоны с истекшим сроком действия и те, по которым сигнал уже сформирован.

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

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

Последний блок OnCalculate обрабатывает перевороты зон, но только если входному параметру EnableZoneFlipping задано значение true. В нем снова перебираются все активные зоны, кроме зон с истекшим сроком действия. Для каждой зоны рассчитываются ее высота и диапазон текущего сигнального бара. Для зон предложения проверяются три условия: цена закрытия находится выше верхней границы зоны, что указывает на пробой; бар бычий, то есть закрывается выше цены открытия; диапазон бара не меньше высоты зоны, умноженной на ViolationMultiplier. Если выполняются все три условия, вызывается FlipZone с новым значением признака зоны спроса true. Для зон спроса проверяется, что бар закрылся ниже нижней границы зоны, является медвежьим и имеет достаточный диапазон. Затем вызывается FlipZone с новым значением признака зоны спроса false. Такой двухпроходный подход обеспечивает переворот зон только при действительно импульсных пробоях, исключая ложные перевороты при незначительных движениях или проколах границы тенью свечи.

Полный исходный код приведен в конце статьи. Скопируйте код в MetaEditor (MT5), скомпилируйте его и добавьте полученный индикатор на график. Затем перейдите в раздел "Тестирование и результаты", чтобы оценить эффективность.


Тестирование и результаты

Мы протестировали индикатор на EURUSD в Тестере стратегий, используя график M5 и обнаружение зон по данным M30. Такая настройка позволила нам проследить за поведением зон старшего таймфрейма на младшем.

На протяжении всего тестирования мы уделяли пристальное внимание механизму переворота. Когда красная зона (предложение) пробивалась вверх импульсным движением, ее цвет сразу менялся на зеленый (спрос). И наоборот, зеленая зона (спрос), уверенно пробитая вниз, меняла цвет на красный. Цвет менялся в реальном времени, и это было легко заметить на графике.

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

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

Ниже представлена анимация с визуализацией стратегии.

Liquidity Zone Flip Indicator Testing

Рис. 3. Индикатор переворота зон ликвидности — визуализация в Тестере стратегий


Заключение

В этой статье мы успешно спроектировали, реализовали и протестировали индикатор на языке MQL5, который выявляет зоны ликвидности на старших таймфреймах по паттернам "база → импульс". При импульсном пробое он динамически переворачивает зоны, меняя их роль с предложения на спрос и наоборот.

Ключевое достижение — механизм динамического переворота зон, который стал значительным шагом вперед по сравнению со статическими инструментами. Когда красная зона предложения пробивалась вверх импульсным движением, ее цвет менялся на зеленый, обозначая превращение в зону спроса, и наоборот. Такое наглядное отображение, которое мы неоднократно наблюдали при тестировании EURUSD (график M5 с обнаружением зон по данным M30), сразу и понятно показывало структурные изменения, которые иначе могли бы остаться незамеченными. Трейдеры могут с первого взгляда определить, когда сетап перестает быть актуальным и когда формируется новая возможность.

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

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

Основные выводы

Основные выводы Описание
Проектирование структуры для сохранения состояния Пользовательская структура (LiquidityZone), инкапсулирующая все свойства зоны, позволяет упорядоченно хранить данные между тиками индикатора и легко добавлять новые возможности.
Управление динамическими массивами с обратным обходом При удалении элементов во время обхода массива нужен обратный порядок, чтобы избежать проблем со сдвигом индексов; такой подход необходим для корректного удаления зон с истекшим сроком действия.
Предотвращение дублирования с помощью уникальных идентификаторов Проверка наличия зон с тем же start_time перед добавлением новых предотвращает дублирование при повторных обновлениях данных старшего таймфрейма. Это критически важный прием для любой системы, обновляющей внешние источники данных.
Соглашения об именовании графических элементов Создание уникальных имен объектов с использованием временных меток (LiqZone_starttime_expirytime) позволяет адресно удалять отдельные объекты графика и управлять ими, не затрагивая другие графические построения.
Индексация как у временных рядов упрощает работу с массивами При установке индексации как у временных рядов (ArraySetAsSeries со значением true) индекс 0 соответствует последнему бару, что значительно упрощает вычисления и повышает читаемость кода.
Отслеживание состояния с помощью булевых флагов Флаги срабатывания в данных зон предотвращают дублирование сигналов и алертов. Это простой, но эффективный прием управления системами, работающими на основе событий, без сложного отслеживания истории.
Декомпозиция логики на отдельные функции Разделение логики распознавания паттернов на отдельные функции (BullishEngulfing, BearishPinBar и т.д.) позволяет создавать пригодные для повторного использования и тестирования компоненты и легко добавлять новые паттерны.
Обработка данных нескольких таймфреймов При копировании данных старших таймфреймов в индикаторе необходимо тщательно управлять буферами, а для оптимизации производительности обновлять данные только при появлении новых баров на этих таймфреймах.
Гибкость за счет параметров Ключевые пороговые значения, заданные входными параметрами (RatioMultiplier, ViolationMultiplier), позволяют пользователям адаптировать индикатор без изменения кода и проводить систематическую оптимизацию.
Ограничение частоты алертов предотвращает усталость от уведомлений Хранение времени последнего алерта и сравнение его со временем текущего бара обеспечивают срабатывание алертов не чаще одного раза на бар. Это критически важно для удобства использования торговых систем, работающих в реальном времени.

Приложения

Имя файла Версия Тип Описание
LiquidityZoneFlipIndicator.mq5 4,00 Исходный код Полный исходный код на языке MQL5 с логикой переворота зон, включая обнаружение зон ликвидности, динамический переворот зон, сигналы реакции и управление сроком действия

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

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
От новичка до эксперта: Усиление стратегий ликвидности за счет структурного подтверждения на нескольких таймфреймах От новичка до эксперта: Усиление стратегий ликвидности за счет структурного подтверждения на нескольких таймфреймах
Согласованность структур ликвидности на старшем таймфрейме и разворотных паттернов на младшем может существенно повлиять как на вероятность, так и на направление следующего движения цены. Сочетая структурные зоны ликвидности на старших таймфреймах с точными подтверждениями разворота на младших, трейдеры могут точнее выбирать момент входа и повышать общее качество сделок. В этой статье показано, как усовершенствовать торговые стратегии на основе ликвидности с помощью структурного подтверждения на старшем таймфрейме и эффективно реализовать этот подход на языке MQL5.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Память между перезапусками: Сохранение состояния советника с помощью двоичных файлов в MQL5 Память между перезапусками: Сохранение состояния советника с помощью двоичных файлов в MQL5
В статье представлена структурированная программная основа MQL5 для сериализации внутреннего состояния советника в локальные двоичные файлы. Она предотвращает сброс данных при перезапусках платформы, надёжно сохраняя непосредственно на диск хранящиеся в оперативной памяти показатели, такие как количество сделок и множители. Данная архитектура представляет собой более надёжную альтернативу глобальным переменным терминала для обеспечения непрерывности состояния.