Использование встроенных индикаторов

В качестве простого вводного примера использования встроенного индикатора возьмем обращение к iStochastic. Прототип этой функции-индикатора следующий:

int iStochastic(const string symbol, ENUM_TIMEFRAMES timeframe,
  int Kperiod, int Dperiod, int slowing,
  ENUM_MA_METHOD method, ENUM_STO_PRICE price)

Как мы видим, помимо стандартных параметров symbol и timeframe, стохастик имеет несколько специфических параметров:

  • Kperiod — количество баров для расчета линии %K;
  • Dperiod — период первичного сглаживания для линии %D;
  • slowing — период вторичного сглаживания (замедления);
  • method — метод устреднения (сглаживания);
  • price — способ расчета стохастика.

Попробуем создать собственный индикатор UseStochastic.mq5, который скопирует к себе в буфера значения стохастика. Поскольку в стохастике 2 буфера, мы также зарезервируем два — они называются "главная" и "сигнальная" линия.

#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
   
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "St'Main"
   
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrChocolate
#property indicator_width2  1
#property indicator_label2  "St'Signal"
#property indicator_style2  STYLE_DOT

Во входных переменных предусмотрим все требуемые параметры.

input int KPeriod = 5;
input int DPeriod = 3;
input int Slowing = 3;
input ENUM_MA_METHOD Method = MODE_SMA;
input ENUM_STO_PRICE StochasticPrice = STO_LOWHIGH;

Далее опишем массивы для индикаторных буферов и глобальную переменную для дескриптора.

double MainBuffer[];
double SignalBuffer[];
   
int Handle;

Инициализацию проведем в OnInit.

int OnInit()
{
   IndicatorSetString(INDICATOR_SHORTNAME,
      StringFormat("Stochastic(%d,%d,%d)"KPeriodDPeriodSlowing));
   // привязка массивов в качестве буферов
   SetIndexBuffer(0MainBuffer);
   SetIndexBuffer(1SignalBuffer);
   // получаем дескриптор Stochastic
   Handle = iStochastic(_Symbol_Period,
      KPeriodDPeriodSlowingMethodStochasticPrice);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

Далее остается дело за малым — в OnCalculate читать данные с помощью функции CopyBuffer по мере готовности дескриптора.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   // ждем расчета стохастика на всех барах
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   // копируем данные в наши два буфера
   const int n = CopyBuffer(Handle00rates_total - prev_calculated + 1,
      MainBuffer);
   const int m = CopyBuffer(Handle10rates_total - prev_calculated + 1,
      SignalBuffer);
   
   return n > -1 && m > -1 ? rates_total : 0;
}

Обратите внимание, что мы два раза вызываем CopyBuffer: для каждого буфера отдельно (0 и 1 во втором параметре). Попытка прочитать буфер с несуществующим индексом, например, 2 — сгенерировала бы ошибку, и мы не получили бы никаких данных.

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

Стандартный стохастик и пользовательский на базе функции iStochastic

Стандартный стохастик и пользовательский на базе функции iStochastic

Чтобы продемонстрировать кэширование индикаторов терминалом, добавим в функцию OnInit пару строк.

   double array[];
   Print("This is very first copy of iStochastic with such settings=",
      !(CopyBuffer(Handle0010array) > 0));

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

Откомпилировав новый индикатор, следует разместить две его копии на двух чартах одного и того же символа и таймфрейма. Первый раз в журнал выведется сообщение с флагом true (это первая копия), а во второй раз (и последующие разы, если графиков много) будет уже false. Также можно сперва вручную набросить на график стандартный индикатор "Stochastic Oscillator" (с настройками по умолчанию или теми, которые затем будут применены в UseStochastic), а затем запустить UseStochastic: мы также должны получить false.

Попробуем теперь придумать нечто оригинальное на базе стандартного индикатора. Следующий индикатор UseM1MA.mq5 предназначен для расчета средних побаровых цен на таймфреймах M5 и выше (преимущественно, внутридневных). Он аккумулирует цены баров M1, попадающих в диапазон временных меток каждого конкретного бара на рабочем (более старшем) таймфрейме. Это позволяет намного более точно оценить эффективную цену бара, чем стандартные типы цен (Close, Open, Median, Typical, Weighted, и т.д.). Дополнительно предусмотрим возможность усреднения таких цен с некоторым периодом, но здесь следует быть готовым, что особо гладкой линии не получится.

Индикатор будет выводиться в главном окне и содержать единственный буфер. Настройки можно менять с помощью 3-х параметров:

input uint _BarLimit = 100// BarLimit
input uint BarPeriod = 1;
input ENUM_APPLIED_PRICE M1Price = PRICE_CLOSE;

BarLimit задает количество баров ближайшей истории для расчета. Он важен, потому что графики на высоких таймфреймах могут потребовать очень большого количества баров при сопоставлении с минутным M1 (например, один день D1 при круглосуточной торговле содержит, как известно, 1440 баров M1). Это может привести к загрузке дополнительных данных и ожиданию синхронизации. Поэкспериментируйте со щадящей настройкой по умолчанию (100 баров рабочего таймфрейма), прежде чем ставить в этом параметре 0, что означает безлимитную обработку.

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

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

Наконец, в параметре M1Price указывается тип цены в расчетах по барам M1.

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

double Buffer[];
   
int Handle;
int BarLimit;
bool PendingRefresh;
   
const string MyName = "M1MA (" + StringSubstr(EnumToString(M1Price), 6)
   + "," + (string)BarPeriod + "[" + (string)(PeriodSeconds() / 60) + "])";
const uint P = PeriodSeconds() / 60 * BarPeriod;

Кроме того, здесь формируется название индикатора и период усреднения P. Функция PeriodSeconds, возвращающая количество секунд внутри одного бара текущего таймфрейма, позволяет рассчитать число баров M1 внутри одного текущего бара: PeriodSeconds() / 60 (60 секунд — длительность бара M1).

Привычная инициализация выполняется в OnInit.

int OnInit()
{
   IndicatorSetString(INDICATOR_SHORTNAMEMyName);
   IndicatorSetInteger(INDICATOR_DIGITS_Digits);
   
   SetIndexBuffer(0Buffer);
   
   Handle = iMA(_SymbolPERIOD_M1P0MODE_SMAM1Price);
   
   return Handle != INVALID_HANDLE ? INIT_SUCCEEDED : INIT_FAILED;
}

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

Функция OnCalculate далее приводится с упрощениями. При первом запуске или изменении истории мы очищаем буфер и заполняем переменную BarLimit (она требуется, потому что входные переменные нельзя редактировать, а мы хотим трактовать значение 0, как максимально доступное количество баров для расчета). При последующих вызовах чистятся элементы буфера только на последних барах, начиная с prev_calculated и не более чем BarLimit.

int OnCalculate(ON_CALCULATE_STD_FULL_PARAM_LIST)
{
   if(prev_calculated == 0)
   {
      ArrayInitialize(BufferEMPTY_VALUE);
      if(_BarLimit == 0
      || _BarLimit > (uint)rates_total)
      {
         BarLimit = rates_total;
      }
      else
      {
         BarLimit = (int)_BarLimit;
      }
   }
   else
   {
      for(int i = fmax(prev_calculated - 1, (int)(rates_total - BarLimit));
         i < rates_total; ++i)
      {
         Buffer[i] = EMPTY_VALUE;
      }
   }

Прежде чем читать данные из созданного индикатора iMA, необходимо дождаться их готовности: для этого сравниваем BarsCalculated с количеством баров M1.

   if(BarsCalculated(Handle) != iBars(_SymbolPERIOD_M1))
   {
      if(prev_calculated == 0)
      {
         EventSetTimer(1);
         PendingRefresh = true;
      }
      return prev_calculated;
   }
   ...

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

Далее мы попадаем в основную расчетную часть алгоритма и потому должны остановить таймер, если он еще запущен. Такое может быть, если очередное событие тика пришло быстрее, чем 1 секунда, и iMA по M1 уже рассчиталась. Логично было бы просто вызвать соответствующую функцию EventKillTimer. Однако в её поведении есть нюанс: она не чистит очередь событий для MQL-программы типа индикатора, и если в очереди уже размещено событие таймера, то обработчик OnTimer будет однократно вызван. Чтобы избежать лишнего обновления графика мы контролируем процесс с помощью собственной переменной PendingRefresh и присваиваем ей здесь false.

   ...
   PendingRefresh = false// данные готовы, таймер сработает вхолостую
   ...

Вот как это все организовано в обработчике OnTimer:

void OnTimer()
{
   EventKillTimer();
   if(PendingRefresh)
   {
      ChartSetSymbolPeriod(0_Symbol_Period);
   }
}

Но вернемся в OnCalculate и представим основной рабочий цикл.

   for(int i = fmax(prev_calculated - 1, (int)(rates_total - BarLimit));
      i < rates_total; ++i)
   {
      static double result[1];
      
      // получим последний бар M1, соответствующий i-му бару текущего таймфрейма
      const datetime dt = time[i] + PeriodSeconds() - 60;
      const int bar = iBarShift(_SymbolPERIOD_M1dt);
      
      if(bar > -1)
      {
         // запросим значение MA на M1
         if(CopyBuffer(Handle0bar1result) == 1)
         {
            Buffer[i] = result[0];
         }
         else
         {
            Print("CopyBuffer failed: "_LastError);
            return prev_calculated;
         }
      }
   }
   
   return rates_total;
}

Работу индикатора иллюстрирует следующее изображение на EURUSD,H1. Голубая линия соответствует настройкам по умолчанию. Каждое значение получено усреднением PRICE_CLOSE по 60 барам M1. Оранжевая линия дополнительно включает сглаживание по 5 барам H1, с ценами M1 PRICE_TYPICAL.

Два экземпляра индикатора UseM1MA на EURUSD,H1

Два экземпляра индикатора UseM1MA на EURUSD,H1

В книге представлена упрощенная версия UseM1MASimple.mq5. Мы оставили за кадром специфику усреднения последнего (неполного) бара, обработку пустых баров (для которых на M1 нет данных) и корректную установку свойства PLOT_DRAW_BEGIN, а также контроль за появлением кратковременных отставаний в расчете средней при появлении новых баров. Полная версия доступна в файле UseM1MA.mq5.

В качестве последнего примера построения индикаторов на основе стандартных разберем усовершенствование индикатора IndUnityPercent.mq5, который был представлен в разделе Мультивалютные и мультитаймфреймовые индикаторы. Первая версия использовала для вычислений цены Close, получая их с помощью CopyBuffer. В новой версии UseUnityPercentPro.mq5 заменим этот способ на чтение показаний индикатора iMA. Это позволит реализовать новые возможности:

  • усреднять цены на заданном периоде;
  • выбирать метод усреднения;
  • выбирать тип цены для расчета;

Изменения в исходном коде — минимальные. Добавим 3 новых параметра и глобальный массив для дескрипторов iMA:

input ENUM_APPLIED_PRICE PriceType = PRICE_CLOSE;
input ENUM_MA_METHOD PriceMethod = MODE_EMA;
input int PricePeriod = 1;
...   
int Handles[];

Во вспомогательной функции InitSymbols, которая вызывается из OnInit для парсинга строки со списком рабочих инструментов, добавим выделение памяти под новый массив (его размер SymbolCount определяется как раз из списка).

string InitSymbols()
{
   SymbolCount = StringSplit(Instruments, ',', Symbols);
   ...
   ArrayResize(HandlesSymbolCount);
   ArrayInitialize(HandlesINVALID_HANDLE);
   ...
   for(int i = 0i < SymbolCounti++)
   {
      ...
      Handles[i] = iMA(Symbols[i], PERIOD_CURRENTPricePeriod0,
         PriceMethodPriceType);
   }
}

В конце этой же функции создадим дескрипторы требуемых подчиненных индикаторов.

В функции Calculate, где выполняется основной расчет, заменим вызовы вида:

CopyClose(Symbols[j], _Periodtime0time1w);

на вызовы:

CopyBuffer(Handles[j], 0time0time1w); // j-й дескриптор, 0-й буфер

Для наглядности мы также дополнили краткое название индикатора указанием трех новых параметров.

   IndicatorSetString(INDICATOR_SHORTNAME,
      StringFormat("Unity [%d] %s(%d,%s)"workCurrencies.getSize(),
      StringSubstr(EnumToString(PriceMethod), 5), PricePeriod,
      StringSubstr(EnumToString(PriceType), 6)));

Вот что получилось в результате.

Мультисимвольный индикатор UseUnityPercentPro с основными парами Forex

Мультисимвольный индикатор UseUnityPercentPro с основными парами Forex

Здесь показана корзина из 8 основных валют Forex (настройка по умолчанию), с усреднением на 11 барах и расчету по цене typical. Две толстые линии соответствуют относительной стоимости валют текущего графика: EUR помечено синим цветом, USD — зеленым.