English Deutsch 日本語
preview
Создание  профессиональной торговой системы на основе Heikin Ashi (Часть 1): Разработка пользовательского индикатора

Создание профессиональной торговой системы на основе Heikin Ashi (Часть 1): Разработка пользовательского индикатора

MetaTrader 5Торговые системы |
49 0
Chacha Ian Maroa
Chacha Ian Maroa

Введение

Технические индикаторы лежат в основе алгоритмической торговли, и умение их создавать — важный этап на пути любого разработчика MQL5. В этой статье мы применим практический подход к разработке пользовательского индикатора, создав один из самых популярных инструментов сглаживания тренда — индикатор Heikin Ashi.

Эта статья — первая часть серии из двух материалов. В первой части мы рассмотрим теорию свечей Heikin Ashi, объясним, как они рассчитываются, и пошагово разберем процесс написания индикатора Heikin Ashi с нуля с использованием MQL5. Цель состоит не просто в копировании и вставке исходного кода, а в настоящем понимании смысла каждой его строки, чтобы вы могли применять эти лучшие практики при создании собственных индикаторов.

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

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

  • Хорошо понимаете язык программирования MQL5. 
  • Умеете пользоваться MetaTrader 5 и MetaEditor 5.
  • Можете добавлять индикаторы и советники на график.
После прочтения этой статьи у вас будет достаточно знаний, чтобы реализовывать пользовательские индикаторы с применением современных лучших практик MQL5. Хотя пример здесь сосредоточен на Heikin Ashi, те же приемы можно применять к любому индикатору, который использует нестандартные свечи или цветовую визуализацию торговых сигналов.



Чем график Heikin Ashi отличается от традиционного свечного графика?

Для начала посмотрим на два графика рядом.

Традиционный свечной график:

Традиционный свечной график

График Heikin Ashi, построенный на основе тех же ценовых данных:

График Heikin Ashi

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

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

В следующем разделе мы подробно разберем, как именно рассчитываются эти свечи.


Как рассчитываются свечи Heikin Ashi

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

Структура свечи

  1. Open — цена в начале формирования свечи.
  2. High — самая высокая цена, достигнутая за время формирования свечи.
  3. Low — самая низкая достигнутая цена.
  4. Close — итоговая цена в конце формирования свечи.

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

Свеча Heikin Ashi состоит из следующих элементов:

  1. Heikin Ashi Open — сглаженная версия цены открытия свечи.
  2. Heikin Ashi High — наибольшее значение из набора, включающего как реальные, так и сглаженные данные.
  3. Heikin Ashi Low — наименьшее значение из такого же набора.
  4. Heikin Ashi Close — средняя цена текущей реальной свечи.

Теперь подробно рассмотрим, как рассчитывается каждое из этих значений свечи Heikin Ashi.


Формулы Heikin Ashi

Heikin Ashi close = (Open + High + Low + Close) / 4.

Это средняя цена текущей свечи. Она дает единое усредненное значение, отражающее общее движение цены за этот период. 

Heikin Ashi open = (open предыдущей свечи Heikin Ashi + close предыдущей свечи Heikin Ashi) / 2.

Это означает, что open текущей свечи Heikin Ashi — не текущее рыночное открытие, а середина тела предыдущей свечи Heikin Ashi. Такой подход помогает сглаживать переходы между свечами и создает характерный «плавный» вид.

Heikin Ashi High = наибольшее значение среди High, Heikin Ashi Open и Heikin Ashi Close.

Heikin Ashi Low = наименьшее значение среди Low, Heikin Ashi Open и Heikin Ashi Close.

Два последних значения объединяют фактические максимумы/минимумы рынка со сглаженными open и close, благодаря чему свеча получает реалистичные тени, сохраняя преимущества сглаживания.

Кратко:

Heikin Ashi close отражает среднее значение цен текущей свечи. Heikin Ashi open зависит от сглаженных значений предыдущей свечи. Heikin Ashi High/Heikin Ashi Low используют как исходные, так и сглаженные значения для представления ценовых максимумов и минимумов.

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

В следующих разделах мы применим эти знания на практике, пошагово создав пользовательский индикатор Heikin Ashi на MQL5.


Планирование логики индикатора

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

Помимо стандартной визуализации, мы улучшим график Heikin Ashi, раскрашивая свечи тремя разными цветами: одним для бычьих свечей, другим для медвежьих и третьим для нейтральных.

Для поддержки этой визуальной логики наш индикатор будет использовать пять буферов:

  1. Heikin Ashi Open. Это массив, в котором хранится цена открытия Heikin Ashi.
  2. Heikin Ashi High. Это массив, в котором хранится максимум Heikin Ashi.
  3. Heikin Ashi Low. Это массив, в котором хранится минимум Heikin Ashi.
  4. Heikin Ashi Close. Это массив, в котором хранится цена закрытия Heikin Ashi.
  5. Color Buffer. Это массив, в котором хранится индекс цвета для каждой свечи Heikin Ashi в зависимости от того, является она бычьей, медвежьей или нейтральной.

Каждый буфер играет важную роль как в построении свечи Heikin Ashi, так и в динамическом применении соответствующего цвета.

Для отображения этих свечей мы будем использовать один графическое построение с DRAW_COLOR_CANDLES, что позволяет использовать один идентификатор графического построения и отдельный буфер для управления цветом. Один момент требует особой аккуратности — самый первый бар на графике, поскольку у него нет предыдущего значения Heikin Ashi. Мы вручную инициализируем значения для первого бара, чтобы остальные свечи можно было корректно построить на основе правильной формулы.

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


Пошаговое руководство по написанию индикатора Heikin Ashi

Для создания нашего пользовательского индикатора Heikin Ashi мы будем использовать язык программирования MQL5. Это основной язык для разработки автоматизированных торговых инструментов в MetaTrader 5.

Поскольку предполагается, что читатели уже умеют работать с MetaTrader 5, MetaEditor и знают основы добавления индикаторов на график, мы пропустим подробности настройки и сразу перейдем к разработке нашего пользовательского индикатора Heikin Ashi.

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

//|                                          heikinAshiIndicator.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                          https://www.mql5.com/en/users/chachaian |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/chachaian"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
  
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread     []) 
{
     
   return(rates_total);
}
//+------------------------------------------------------------------+

Разберем этот код на простые логические части, чтобы его было легче понять.

Директивы свойств:

...
#property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian"
#property link      "https://www.mql5.com/en/users/chachaian"
#property version   "1.00"

...

Эти строки задают метаинформацию об индикаторе, которая будет отображаться в навигаторе MetaTrader 5 и в окне описания.

  • #property copyright. Объявляет правообладателя и разработчика. Полезно для брендинга и обозначения принадлежности кода.
  • #property link. Указывает кликабельную ссылку на файл проекта или страницу проекта.
  • #property version. Полезно для контроля версий при будущих обновлениях.
Функция инициализации:

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   return(INIT_SUCCEEDED);
}

...

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

Функция расчета:

...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread[]) 
{

   return(rates_total);
}

...

Это сердце вашего индикатора. Эта функция вызывается многократно по мере формирования новых свечей. Ее параметры предоставляют доступ к:

  • Rates_total. Обозначает общее количество доступных баров.
  • Prev_calculated. Показывает, сколько баров было обработано при последнем вызове функции 'OnCalculate' (для оптимизации).
  • Переменным time, open, high, low и close, которые фактически являются нашими историческими ценовыми рядами.
  • Volume, tick_volume и spread, то есть дополнительным торговым данным.

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

В MetaTrader 5 индикаторы могут отображаться двумя основными способами: в главном окне графика поверх цены или в отдельном подокне под основным графиком.

Чтобы определить способ отображения индикатора, мы используем либо #property indicator_chart_window для отображения в главном окне графика поверх цены, либо #property indicator_sub_window для индикаторов в отдельном подокне. Поскольку мы создаем индикатор, который отображается в главном окне графика поверх цены, нам нужен именно этот вариант.

С учетом этого добавим следующую строку сразу под существующими директивами #property:

#property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian"
#property link      "https://www.mql5.com/en/users/chachaian"
#property version   "1.00"
#property indicator_chart_window

...

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

Прежде чем использовать их в исходном коде, нужно указать, сколько буферов будет использовать ваш индикатор. Это делается с помощью директивы #property indicator_buffers N. Здесь N обозначает общее количество буферов, необходимых индикатору. В нашем случае мы будем использовать 5 буферов, поэтому объявим это так: #property indicator_buffers 5.

Добавим следующую строку сразу под другими существующими директивами в нашем исходном коде.

...

#property indicator_chart_window
#property indicator_buffers 5
...

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

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

Добавим следующую строку кода сразу под другими существующими директивами #property.

...
#property indicator_chart_window
#property indicator_buffers 5
#property indicator_plots   1
...

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

...
#property indicator_plots   1

//Global variables
double haOpen [];
double haHigh [];
double haLow  [];
double haClose[];
double colorBuffer [];
...

Теперь, когда массивы объявлены, следующий шаг — зарегистрировать каждый из них как буфер индикатора с помощью встроенной функции MQL5 SetIndexBuffer(). Эта функция связывает объявленные массивы с системой индикаторов, чтобы MetaTrader мог использовать их для отрисовки и расчетов. Мы сделаем это внутри функции OnInit(), регистрируя каждый массив по очереди.

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   
   // Registration of indicator buffers
   if(!SetIndexBuffer(0, haOpen, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(1, haHigh, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(2, haLow,   INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(3, haClose, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(4, colorBuffer, INDICATOR_COLOR_INDEX)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
}

...

Далее настроим основную логику нашего индикатора внутри функции OnCalculate(). Эта функция вызывается каждый раз при обновлении индикатора: когда он впервые добавляется на график, когда формируется новая свеча или когда поступает новый ценовой тик.

Обычно мы структурируем эту функцию в виде трех основных условных блоков, чтобы эффективно обрабатывать каждый из этих сценариев:

  1. Когда индикатор впервые добавляется на график, выполняется условие prev_calculated == 0. Этот блок используется для выполнения логики инициализации, например заполнения исторических значений или задания начальных условий.
  2. Когда открывается новая свеча, выполняется условие prev_calculated != rates_total и prev_calculated != 0. Здесь можно выполнять логику, которая должна срабатывать только один раз на новый бар. Это полезно для сокращения повторяющихся расчетов.
  3. Когда на текущем баре поступает новый ценовой тик, выполняется условие prev_calculated == rates_total. В этом блоке мы обновляем значения текущей свечи в реальном времени, используя последние ценовые данные.

Ниже приведена базовая структура, которую мы включим в нашу функцию OnCalculate():

...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread[]) 
{
   // This block is executed when the indicator is initially attached on a chart
   if(prev_calculated == 0){
   }
   
   // This block is executed on every new bar open
   if(prev_calculated != rates_total && prev_calculated != 0){
   }
   
   // This block is executed on arrival of new price (tick) data
   if(prev_calculated == rates_total){
   }
   
   return(rates_total);
}

...

Далее мы определим цветовую палитру для наших свечей Heikin Ashi с помощью директивы #property indicator_color1. Эта директива позволяет указать цвета по умолчанию для графического построения, который мы определили ранее. Для нашего индикатора мы будем использовать три цвета:

  1. clrDarkGreen для бычьих свечей Heikin Ashi.
  2. clrDarkRed для медвежьих свечей Heikin Ashi.
  3. clrYellow для нейтральных свечей.

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

...
#property indicator_plots   1
#property indicator_color1 clrDarkGreen, clrDarkRed, clrYellow
...

Теперь добавим директиву, которая задает описательные имена для каждого буфера индикатора. Это помогает пользователям понять, что означает каждая строка при просмотре данных в окне данных MetaTrader. Добавьте следующую строку кода сразу под существующими директивами #property:

...
#property indicator_color1 clrDarkGreen, clrDarkRed, clrYellow
#property indicator_label1 "HeikinAshiOpen;HeikinAshiHigh;HeikinAshiLow;HeikinAshiClose"
...

Прежде чем реализовывать полную логику внутри обработчика событий OnCalculate, сначала определим несколько вспомогательных функций, которые будут отвечать за расчеты Heikin Ashi и визуальную интерпретацию. Эти служебные функции будут:

  • Вычислять значения Heikin Ashi (OHLC) на основе обычных ценовых данных.
  • Определять визуальное направление (цвет) каждой свечи Heikin Ashi.
  • Разделять полную обработку истории и обновления в реальном времени для повышения эффективности.

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

...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread     []) 
{
   ...
    
   return(rates_total);
}

// Utility functions

//+----------------------------------------------------------------------------------+
//| Calculates Heikin Ashi values for all historical candles using price data arrays.|
//+----------------------------------------------------------------------------------+
void GetHeikinAshiValues(const double &open[], const double &high[], const double &low[], const double &close[], const int32_t rates_total)
{ 

   if(ArraySize(open) < rates_total){
      return;
   }
    
   // Run a loop through all historical bars
   for(int i=0; i<rates_total; i++){      
      if(i == 0){
         haOpen [i] = (open[i] + close[i]) / 2.0;
         haClose[i] = (open[i] + high[i] + low[i] + close[i]) / 4.0;
         haHigh [i] = MathMax(high[i], MathMax(open[i], close[i]));
         haLow  [i] = MathMin(low [i], MathMin(open[i], close[i]));
      }else{
         haOpen [i] = (haOpen[i-1] + haClose[i-1]) / 2.0;
         haClose[i] = (open[i] + high[i] + low[i] + close[i]) / 4.0;
         haHigh [i] = MathMax(high[i], MathMax(haOpen[i], haClose[i]));
         haLow  [i] = MathMin(low [i], MathMin(haOpen[i], haClose[i]));  
      }
   }
   
}


//+---------------------------------------------------------------------------------------+
//| Calculates Heikin Ashi values for the most recent candle only (for real-time updates).|
//+---------------------------------------------------------------------------------------+
void GetCurrentHeikinAshiValue(const double &open[], const double &high[], const double &low[], const double &close[], const int32_t rates_total)
{
   haOpen [rates_total - 1] = (haOpen[rates_total-2] + haClose[rates_total-2]) / 2.0;
   haClose[rates_total - 1] = (open[rates_total - 1] + high[rates_total - 1] + low[rates_total - 1] + close[rates_total - 1]) / 4.0;
   haHigh [rates_total - 1] = MathMax(high[rates_total - 1], MathMax(haOpen[rates_total - 1], haClose[rates_total - 1]));
   haLow  [rates_total - 1] = MathMin(low [rates_total - 1], MathMin(haOpen[rates_total - 1], haClose[rates_total - 1])); 
}


//+------------------------------------------------------------------------------------------------------------------+
//| Assigns a color code to each historical Heikin Ashi candle based on its direction (bullish, bearish, or neutral).|
//+------------------------------------------------------------------------------------------------------------------+
void GetHeikinAshiColors(const int32_t rates_total){
   
   for(int i=0; i<rates_total; i++){
      if(haOpen[i] < haClose[i]){
         colorBuffer[i] = 0;
      }
      
      else if(haOpen[i] > haClose[i]){
         colorBuffer[i] = 1;
      }
      
      else {
         colorBuffer[i] = 2;
      }
   }
   
}

//+-----------------------------------------------------------------------------------------------+
//| Assigns a color code to the latest Heikin Ashi candle only (used for real-time color updates).|
//+-----------------------------------------------------------------------------------------------+
void GetCurrentHeikinAshiColor(const int32_t rates_total){
      if(haOpen[rates_total - 1] < haClose[rates_total - 1]){
         colorBuffer[rates_total - 1] = 0;
      }
      
      else if(haOpen[rates_total - 1] > haClose[rates_total - 1]){
         colorBuffer[rates_total - 1] = 1;
      }
      
      else {
         colorBuffer[rates_total - 1] = 2;
      }
}

...

Теперь вызовем функцию GetHeikinAshiValues() внутри обработчика событий OnCalculate(), а именно при следующих условиях:

  • Когда prev_calculated == 0, то есть индикатор загружается впервые и нам нужно обработать все исторические свечи.
  • Когда prev_calculated != rates_total и prev_calculated != 0, то есть были добавлены новые бары или график был обновлен, поэтому нужно пересчитать данные с того места, где мы остановились.

Эта функция отвечает за расчет всей серии свечей Heikin Ashi, формируя их значения open, high, low и close на основе стандартных массивов ценовых данных. Она гарантирует, что наши буферы корректно заполняются с самого начала или правильно обновляются при поступлении новых данных.

...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread     []) 
{
   // This block is executed when the indicator is initially attached on a chart
   if(prev_calculated == 0){
      GetHeikinAshiValues(open, high, low, close, rates_total);
   }
   
   // This block is executed on every new bar open
   if(prev_calculated != rates_total && prev_calculated != 0){
      GetHeikinAshiValues(open, high, low, close, rates_total);
   }
   
   // This block is executed on arrival of new price (tick) data
   if(prev_calculated == rates_total){
   }
   
   return(rates_total);
}

...

Далее вызовем функцию GetHeikinAshiColors() внутри тех же условных блоков в OnCalculate(). То есть:

  • Когда prev_calculated == 0, раскрасить все исторические свечи при начальной загрузке.
  • Когда prev_calculated != rates_total и prev_calculated != 0, обновить цвета для обновленного набора данных.

Эта функция назначает каждой свече Heikin Ashi числовой код цвета на основе соотношения между open и close:

  • 0 для бычьих свечей.
  • 1 для медвежьих свечей.
  • 2 для нейтральных свечей.

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

...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread     []) 
{
   // This block is executed when the indicator is initially attached on a chart
   if(prev_calculated == 0){
      ...
      GetHeikinAshiColors(rates_total);
   }
   
   // This block is executed on every new bar open
   if(prev_calculated != rates_total && prev_calculated != 0){
      ...
      GetHeikinAshiColors(rates_total);
   }
   
   // This block is executed on arrival of new price (tick) data
   if(prev_calculated == rates_total){
   }
   
   return(rates_total);
}

...

Наконец, вызовем функции GetCurrentHeikinAshiValue() и GetCurrentHeikinAshiColor() внутри блока if(prev_calculated == rates_total){}. Этот блок выполняется, когда индикатор обновляется в реальном времени, то есть когда приходит новый тик, но новая свеча еще не сформировалась.

Вызывая:

  • GetCurrentHeikinAshiValue(), мы гарантируем, что самые последние значения Heikin Ashi пересчитываются для текущей свечи с использованием последних тиковых данных.
  • GetCurrentHeikinAshiColor(), мы сразу назначаем соответствующий код цвета для последней свечи Heikin Ashi.

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

...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread     []) 
{
   ...  
   
   // This block is executed on arrival of new price (tick) data
   if(prev_calculated == rates_total){
      GetCurrentHeikinAshiValue(open, high, low, close, rates_total);
      GetCurrentHeikinAshiColor(rates_total);
   }
   
   
   return(rates_total);
}

...

Теперь мы поочередно настроим наш графическое построение с помощью функции PlotIndexSetInteger(), чтобы индикатор Heikin Ashi отображался при запуске. Мы будем задавать настройки одну за другой, определяя, как наши буферы будут отрисовываться на графике, включая тип отрисовки.

Эти настройки необходимы терминалу MetaTrader 5, чтобы понять, как строить рассчитанные нами значения. После завершения этого шага индикатор Heikin Ashi наконец станет визуально активным и будет отображаться поверх графика при применении. Разберем эти настройки. Начнем с настройки графического построения, вызвав функцию PlotIndexSetInteger() для задания типа отрисовки. Эта строка сообщает MetaTrader, что наш графическое построение нужно отображать с помощью цветных свечей. Мы поместим ее сразу под вызовами SetIndexBuffer() внутри обработчика событий OnInit(). Если настройка не выполнится, в журнал экспертов будет выведено сообщение об ошибке для отладки.

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   ...
  
   if(!SetIndexBuffer(4, colorBuffer, INDICATOR_COLOR_INDEX)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   // Configuration of Graphic Plots
   if(!PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_COLOR_CANDLES)){
      Print("Error while configuring graphic plots: ", GetLastError());
      return INIT_FAILED;
   }
   
}

...

Теперь убедимся, что данные Heikin Ashi отображаются в окне данных при наведении мыши на график. Для этого используем PlotIndexSetInteger() с PLOT_SHOW_DATA, установленным в true. Эта строка также должна идти сразу после предыдущей внутри функции OnInit(). Если она не сработает, будет выведено сообщение об ошибке.

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   ...
   
   // Configuration of Graphic Plots
   if(!PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_COLOR_CANDLES)){
      Print("Error while configuring graphic plots: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!PlotIndexSetInteger(0, PLOT_SHOW_DATA, true)){
      Print("Error while configuring graphic plots: ", GetLastError());
      return INIT_FAILED;
   }
   
}

...

Далее настроим несколько общих параметров индикатора. Первый параметр определит, сколько десятичных знаков (digits) должны иметь значения индикатора, с помощью IndicatorSetInteger() и INDICATOR_DIGITS. Это помогает обеспечить соответствие значений индикатора точности символа. Если что-то пойдет не так, мы выведем сообщение об ошибке для отладки.

Добавим следующие строки кода ниже функций PlotIndexSetInteger() в разделе OnInit().

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   ...
   
   if(!PlotIndexSetInteger(0, PLOT_SHOW_DATA, true)){
      Print("Error while configuring graphic plots: ", GetLastError());
      return INIT_FAILED;
   }
   
   // Configure Indicator
   if(!IndicatorSetInteger(INDICATOR_DIGITS, Digits())){
      Print("Error while setting indicator values accuracy: ", GetLastError());
      return INIT_FAILED;
   }
   
}

...

Далее зададим индикатору короткое имя с помощью IndicatorSetString(). Это имя, "HeikinAshi,", будет отображаться в окне данных и на графике, помогая пользователям легко определить индикатор. Если имя не удастся установить, мы выведем сообщение об ошибке, чтобы понять, что пошло не так.

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   ...
   
   // Configure Indicator
   if(!IndicatorSetInteger(INDICATOR_DIGITS, Digits())){
      Print("Error while setting indicator values accuracy: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!IndicatorSetString(INDICATOR_SHORTNAME, "HeikinAshi")){
      Print("Error while setting indicator shortname: ", GetLastError());
      return INIT_FAILED;
   }
   
   return INIT_SUCCEEDED;
   
}

...

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

//+------------------------------------------------------------------+
//|                                          heikinAshiIndicator.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                          https://www.mql5.com/en/users/chachaian |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/chachaian"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 5
#property indicator_plots   1
#property indicator_color1 C'38,166,154', C'239,83,80', clrYellow
#property indicator_label1 "HeikinAshiOpen;HeikinAshiHigh;HeikinAshiLow;HeikinAshiClose"

//Global variables
double haOpen      [];
double haHigh      [];
double haLow       [];
double haClose     [];
double colorBuffer [];

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{  
   //--- Registration of indicator buffers
   if(!SetIndexBuffer(0, haOpen, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(1, haHigh, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(2, haLow,   INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(3, haClose, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!SetIndexBuffer(4, colorBuffer, INDICATOR_COLOR_INDEX)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
   
   //--- Configuration of graphic plots
   if(!PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_COLOR_CANDLES)){
      Print("Error while configuring graphic plots: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!PlotIndexSetInteger(0, PLOT_SHOW_DATA, true)){
      Print("Error while configuring graphic plots: ", GetLastError());
      return INIT_FAILED;
   }
   
   //--- General indicator configurations
   if(!IndicatorSetInteger(INDICATOR_DIGITS, Digits())){
      Print("Error while setting indicator values accuracy: ", GetLastError());
      return INIT_FAILED;
   }
   
   if(!IndicatorSetString(INDICATOR_SHORTNAME, "HeikinAshi")){
      Print("Error while setting indicator shortname: ", GetLastError());
      return INIT_FAILED;
   }
   
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t  rates_total,
                const int32_t  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 int32_t  &spread     []) 
{
   //--- This block is executed when the indicator is initially attached on a chart
   if(prev_calculated == 0){
      GetHeikinAshiValues(open, high, low, close, rates_total);
      GetHeikinAshiColors(rates_total);
   }
   
   //--- This block is executed on every new bar open
   if(prev_calculated != rates_total && prev_calculated != 0){
      GetHeikinAshiValues(open, high, low, close, rates_total);
      GetHeikinAshiColors(rates_total);
   }
   
   //--- This block is executed on arrival of new price (tick) data
   if(prev_calculated == rates_total){
      GetCurrentHeikinAshiValue(open, high, low, close, rates_total);
      GetCurrentHeikinAshiColor(rates_total);
   }
   
   return(rates_total);
}

//--- Utility functions
//+----------------------------------------------------------------------------------+
//| Calculates Heikin Ashi values for all historical candles using price data arrays.|
//+----------------------------------------------------------------------------------+
void GetHeikinAshiValues(const double &open[], const double &high[], const double &low[], const double &close[], const int32_t rates_total)
{ 

   if(ArraySize(open) < rates_total){
      return;
   }
    
   //--- Run a loop through all historical bars
   for(int i=0; i<rates_total; i++){      
      if(i == 0){
         haOpen [i] = (open[i] + close[i]) / 2.0;
         haClose[i] = (open[i] + high[i] + low[i] + close[i]) / 4.0;
         haHigh [i] = MathMax(high[i], MathMax(open[i], close[i]));
         haLow  [i] = MathMin(low [i], MathMin(open[i], close[i]));
      }else{
         haOpen [i] = (haOpen[i-1] + haClose[i-1]) / 2.0;
         haClose[i] = (open[i] + high[i] + low[i] + close[i]) / 4.0;
         haHigh [i] = MathMax(high[i], MathMax(haOpen[i], haClose[i]));
         haLow  [i] = MathMin(low [i], MathMin(haOpen[i], haClose[i]));  
      }
   }
   
}

//+---------------------------------------------------------------------------------------+
//| Calculates Heikin Ashi values for the most recent candle only (for real-time updates).|
//+---------------------------------------------------------------------------------------+
void GetCurrentHeikinAshiValue(const double &open[], const double &high[], const double &low[], const double &close[], const int32_t rates_total)
{
   haOpen [rates_total - 1] = (haOpen[rates_total-2] + haClose[rates_total-2]) / 2.0;
   haClose[rates_total - 1] = (open[rates_total - 1] + high[rates_total - 1] + low[rates_total - 1] + close[rates_total - 1]) / 4.0;
   haHigh [rates_total - 1] = MathMax(high[rates_total - 1], MathMax(haOpen[rates_total - 1], haClose[rates_total - 1]));
   haLow  [rates_total - 1] = MathMin(low [rates_total - 1], MathMin(haOpen[rates_total - 1], haClose[rates_total - 1])); 
}


//+------------------------------------------------------------------------------------------------------------------+
//| Assigns a color code to each historical Heikin Ashi candle based on its direction (bullish, bearish, or neutral).|
//+------------------------------------------------------------------------------------------------------------------+
void GetHeikinAshiColors(const int32_t rates_total)
{
   
   for(int i=0; i<rates_total; i++){
      if(haOpen[i] < haClose[i]){
         colorBuffer[i] = 0;
      }
      
      if(haOpen[i] > haClose[i]){
         colorBuffer[i] = 1;
      }
      
      if(haOpen[i] == haClose[i]){
         colorBuffer[i] = 2;
      }
   }
   
}

//+-----------------------------------------------------------------------------------------------+
//| Assigns a color code to the latest Heikin Ashi candle only (used for real-time color updates).|
//+-----------------------------------------------------------------------------------------------+
void GetCurrentHeikinAshiColor(const int32_t rates_total)
{
      if(haOpen[rates_total - 1] < haClose[rates_total - 1]){
         colorBuffer[rates_total - 1] = 0;
      }
      
      else if(haOpen[rates_total - 1] > haClose[rates_total - 1]){
         colorBuffer[rates_total - 1] = 1;
      }
      
      else {
         colorBuffer[rates_total - 1] = 2;
      }
}
//+------------------------------------------------------------------+


Тестирование и визуальная настройка

Перед тем как начать визуально тестировать индикатор Heikin Ashi, полезно привести график в порядок, чтобы все было легко видеть. Мы определим небольшую функцию, которая именно это и делает. Она настраивает фон, сетку и цветовые параметры, чтобы график выглядел аккуратно, а свечи были хорошо видны. Вот код:

...

//+-----------------------------------------------------------------------------------------------+
//| Assigns a color code to the latest Heikin Ashi candle only (used for real-time color updates).|
//+-----------------------------------------------------------------------------------------------+ 
void GetCurrentHeikinAshiColor(const int32_t rates_total){
      if(haOpen[rates_total - 1] < haClose[rates_total - 1]){
         colorBuffer[rates_total - 1] = 0;
      }
      
      else if(haOpen[rates_total - 1] > haClose[rates_total - 1]){
         colorBuffer[rates_total - 1] = 1;
      }
      
      else {
         colorBuffer[rates_total - 1] = 2;
      }
}

//+-------------------------------------------------+
//| This function configures the chart's appearance.|
//+-------------------------------------------------+
bool ConfigureChartAppearance()
{
   if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite)){
      Print("Error while setting chart background, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){
      Print("Error while setting chart grid, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_MODE, CHART_LINE)){
      Print("Error while setting chart mode, ", GetLastError());
      return false;
   }

   if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){
      Print("Error while setting chart foreground, ", GetLastError());
      return false;
   }
   
   return true;
}

Считайте эту функцию персональным стилистом вашего графика. Она выполняет следующее:

  • Устанавливает белый фон для наглядности.
  • Убирает сетку, чтобы график выглядел чище.
  • Меняет тип графика на линейный, чтобы он не мешал пользовательским свечам.
  • Обеспечивает черный цвет переднего плана для хорошего контраста.

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

...

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{

   if(!ConfigureChartAppearance()){
      Print("Error while configuring chart appearance: ", GetLastError());
      return INIT_FAILED;
   }
   
   // Registration of indicator buffers
   if(!SetIndexBuffer(0, haOpen, INDICATOR_DATA)){
      Print("Error while registering an indicator buffer: ", GetLastError());
      return INIT_FAILED;
   }
      
   ...
   
}

...

Мы успешно завершили создание нашего пользовательского индикатора Heikin Ashi. Все готово: от расчета свечей Heikin Ashi до их аккуратной отрисовки на графике с хорошо настроенным внешним видом. Теперь пора добавить индикатор на график и увидеть его в действии.

график золота H1

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


Заключение

В этой части мы успешно создали полнофункциональный пользовательский индикатор Heikin Ashi на MQL5. Мы прошли каждый шаг: от настройки буферов и расчета значений до настройки внешнего вида графика, а затем добавили индикатор на график золота с таймфреймом H1. Мы подтвердили, что все работает так, как ожидалось. Чтобы вам было проще следовать материалу или устранять неполадки, к этому уроку приложены как полный исходный код, так и скомпилированная версия.

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

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

Прикрепленные файлы |
Разработка торговой стратегии с использованием подхода Volume Boundary Разработка торговой стратегии с использованием подхода Volume Boundary
В мире технического анализа цена часто оказывается в центре внимания. Трейдеры тщательно размечают поддержку, сопротивление и паттерны, но нередко игнорируют ключевую силу, которая движет этими ценовыми движениями: объем. В этой статье рассматривается новый подход к анализу объема — индикатор Volume Boundary. Такое преобразование с использованием сложных сглаживающих функций, таких как кривая «бабочка» и тройная синусоида, облегчает интерпретацию данных и позволяет разрабатывать системные торговые стратегии.
Рыночные секреты Ларри Уильямса (Часть 1): Создание индикатора свинговой структуры рынка в MQL5 Рыночные секреты Ларри Уильямса (Часть 1): Создание индикатора свинговой структуры рынка в MQL5
Практическое руководство по созданию индикатора рыночной структуры в стиле Ларри Уильямса на MQL5: настройка буферов, определение свинговых точек, настройка графических построений и применение индикатора трейдерами в техническом анализе рынка.
Изучение стандартной библиотеки MQL5 (часть 1): Знакомство с CTrade, CiMA и CiATR Изучение стандартной библиотеки MQL5 (часть 1): Знакомство с CTrade, CiMA и CiATR
Стандартная библиотека MQL5 — чрезвычайно полезный инструмент при разработке торговых алгоритмов для MetaTrader 5. В этой серии мы будем учиться создавать с помощью нее эффективные торговые инструменты для MetaTrader 5. Под инструментами подразумеваются собственные советники, индикаторы и другие вспомогательные средства. Сегодня мы разработаем трендового советника с использованием классов CTrade, CiMA и CiATR. Тема будет полезна всем — и начинающим, так и опытным разработчикам. Приятного чтения.
Знакомство с языком MQL5 (Часть 43): Руководство для начинающих по работе с файлами в MQL5 (V) Знакомство с языком MQL5 (Часть 43): Руководство для начинающих по работе с файлами в MQL5 (V)
В статье объясняется, как использовать структуры MQL5 вместе с бинарными файлами, чтобы сохранять параметры советника между запусками. В статье рассматриваются определение структур, доступ к их членам и различие между простыми и сложными структурами, а затем запись и чтение структур целиком с помощью FileWriteStruct и FileReadStruct в режиме FILE_BIN. Вы узнаете о безопасных подходах к работе с данными фиксированного размера и о том, как общее хранилище (FILE_COMMON) позволяет использовать одни и те же данные между сеансами и терминалами.