Как написать для Маркета индикатор любых нестандартных графиков

Vladimir Karputov | 29 апреля, 2016

Оглавление

 

От японских свечей до графиков Ренко

На сегодняшний день самый популярный вид графиков среди трейдеров — японские свечи, которые помогают легко оценить текущую ситуацию на рынке. Свечные графики дают хорошее и наглядное представление о развитии цены в течение промежутка времени, охватываемого одной свечой. Но некоторые трейдеры считают недостатком то, что графики несут в себе компонент времени, и предпочитают иметь дело только с изменением цены. Так появились графики "Крестики-Нолики", "Ренко", "Каги", "Range bars", эквиобъемные графики и т.п. 

С помощью оффлайновых графиков, программирования на языке MQL4 и небольшого опыта все эти графики вы можете получить в MetaTrader 4. Есть возможность создавать графики с собственными синтетическими инструментами (которых нет у брокера или даже вовсе в природе) и нестандартными таймфреймами, которых нет в платформе. Большинство разработчиков используют для этого вызовы DLL и сложные схемы. В этой статье мы расскажем, как создать индикаторы вида "два в одном" любой сложности,  которые не только не требуют знаний DLL, но и могут быть легко опубликованы в Маркете в виде продукта, поскольку являются полностью независимыми и законченными приложениями.

Примеры из этой статьи вы сможете найти и скачать в виде бесплатных приложений Маркета:

Индикатору, который создаёт автономный график нестандартного символа и/или периода нет необходимости вызывать DLL — всё решается средствами MQL4. Для этого нужно принять следующую схему работы: один и тот же индикатор может работать как на онлайн-графике, так и на оффлайновом графике. При этом индикатор будет менять свой функционал в зависимости от того, где он работает: на онлайн-графике или в автономном режиме.

На онлайн-графике индикатор работает в режиме "обслуживание": собирает и компонует котировки, создаёт автономный график (как стандартного, так и нестандартного периода), осуществляет его обновление. На автономном графике такой индикатор работает так же, как и любой другой — анализирует котировки и  строит различные объекты и фигуры. Отметим, что все примеры в статье базируются на новом штатном скрипте PeriodConverter.mq4.


1. Индикатор "IndCreateOffline", который будет создавать автономный график

Назовем индикатор IndCreateOffline. В индикаторе будет только один входной параметр, отвечающий за период автономного графика. Поговорим про него немного ниже. Индикатор IndCreateOffline будет выполнять только одну задачу — создавать файл *.hst и открывать автономный график.

В индикаторе будут использоваться две основные функции. Первая используется один раз — в ней создаётся сам файл *.hst и оформляется заголовок файла. Вторая функция служит для записи котировок в файл *.hst.

1.1. Редактирование "шапки" индикатора

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

#property version   "1.00"
#property description "The indicator creates an offline chart"
#property strict
#property indicator_chart_window

Конечно, теоретически автономный график можно создать с любым периодом. Однако мы ограничим буйную трейдерскую фантазию на случай, если кто-то вдруг заинтересуется периодом эдак 10000000. Введем перечисление, в котором можно выбрать всего четыре варианта — две, три, четыре или шесть минут:

#property indicator_chart_window
//+------------------------------------------------------------------+
//| Enumerations of periods offline chart                            |
//+------------------------------------------------------------------+
enum ENUM_OFF_TIMEFRAMES
  {
   M2=2,                      // period M2
   M3=3,                      // period M3
   M4=4,                      // period M4
   M6=6,                      // period M6
  };
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |

Следующий шаг — добавление в шапку глобальных переменных (не путайте их с глобальными переменными терминала):

   M6=6,                      // period M6
  };
//--- input parameter
input ENUM_OFF_TIMEFRAMES  ExtOffPeriod=M6;
//---
bool     crash=false;         // false -> error in the code
int      HandleHistory=-1;    // handle for the opened "*.hst" file
datetime time0;               //
ulong    last_fpos=0;         //
long     last_volume=0;       //
int      periodseconds;       //
int      i_period;            //
MqlRates rate;                //
long     ChartOffID=-1;       // ID of the offline chart
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |

1.2. Контроль типа графика

Наш индикатор IndCreateOffline должен запускаться только на онлайн-графике, потому что только так можно обеспечить корректность данных. Определить тип графика, на котором установлен индикатор, можно при помощи свойства CHART_IS_OFFLINE. Добавим после OnCalculate() функцию IsOffline, которая будет возвращать тип графика:

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+ 
//| The function checks offline mode of the chart                    | 
//+------------------------------------------------------------------+ 
bool IsOffline(const long chart_ID=0)
  {
   bool offline=ChartGetInteger(chart_ID,CHART_IS_OFFLINE);
   return(offline);
  }
//+------------------------------------------------------------------+

Вызов функции IsOffline будем делать в OnInit():

int OnInit()
  {
   if(!IsOffline(ChartID()) && Period()!=PERIOD_M1)
     {
      Print("The period on the online chart must be \"M1\"!");
      crash=true;
     }
//---
   return(INIT_SUCCEEDED);
  }

Обратите внимание, что если индикатор находится на онлайн-графике (IsOffline(ChartID())==false), период которого не равен PERIOD_M1, то в таком случае переменной crash присваивается значение true. Что это даёт: при crash==true индикатор будет оставаться на онлайн-графике, но не будет ничего делать. В этом случае мы получим во вкладке "Эксперты" такое сообщение:

IndCreateOffline EURUSD,H1: The period on the online chart must be "M1"!

Однако сам индикатор будет оставаться на онлайн-графике и будет ожидать, пока пользователь сменит период на PERIOD_M1.

Почему нам так важен именно период PERIOD_M1? Здесь действуют два важных момента.

Момент 1: формирование итогового периода автономного графика. Рассмотрим пример скрипта PeriodConverter.mq4,  где высчитывается итоговый период автономного графика:

#property show_inputs
input int InpPeriodMultiplier=3; // Period multiplier factor
int       ExtHandle=-1;
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   datetime time0;
   ulong    last_fpos=0;
   long     last_volume=0;
   int      i,start_pos,periodseconds;
   int      cnt=0;
//---- History header
   int      file_version=401;
   string   c_copyright;
   string   c_symbol=Symbol();
   int      i_period=Period()*InpPeriodMultiplier;
   int      i_digits=Digits;
   int      i_unused[13];
   MqlRates rate;
//---  

При таких входных параметрах период онлайн-графика, на который прикрепляется скрипт, равен "PERIOD_M3". При значении InpPeriodMultiplier=3 мы ожидаем получения автономного графика с периодом 3. Однако на самом деле мы получим период автономного графика, равный 9:

   i_period=Period()*InpPeriodMultiplier=3*3=9

Таким образом, чтобы получить период 3, нужно использовать онлайн-график с периодом PERIOD_M1. 

Момент 2: запись истории в файл. При формировании файла истории используются данные из массивов-таймсерий Open[], Low[], High[], Volume[], Time[]. Все они используют данные текущего графика по текущему периоду. А что может быть точнее, чем формирование любого искусственного периода на основании данных с графика с периодом "PERIOD_M1"? Правильно: только график с периодом PERIOD_M1. 

Изменения индикатора, описанные выше, можно увидеть в файле IndCreateOfflineStep1.mq4.

1.3. Функция создания заголовка файла истории

Функцию, отвечающую за создание заголовка файла истории, назовём CreateHeader():

bool CreateHeader(
   const ENUM_OFF_TIMEFRAMES offline_period // period of offline chart
   );

Параметры

offline_period

[in]  Период автономного графика. 

Возвращаемое значение

true, если файл истории был удачно создан, и false — в случае ошибки.

Полный листинг функции:

//+------------------------------------------------------------------+ 
//| The function checks offline mode of the chart                    | 
//+------------------------------------------------------------------+ 
bool IsOffline(const long chart_ID=0)
  {
   bool offline=ChartGetInteger(chart_ID,CHART_IS_OFFLINE);
   return(offline);
  }
//+------------------------------------------------------------------+
//| Create history header                                            |
//+------------------------------------------------------------------+
bool CreateHeader(const ENUM_OFF_TIMEFRAMES offline_period)
  {
//---- History header
   int      file_version=401;
   string   c_copyright;
   string   c_symbol=Symbol();
   i_period=Period()*offline_period;
   int      i_digits=Digits;
   int      i_unused[13];
//---  
   ResetLastError();
   HandleHistory=FileOpenHistory(c_symbol+(string)i_period+".hst",FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ|FILE_ANSI);
   if(HandleHistory<0)
     {
      Print("Error open ",c_symbol+(string)i_period,".hst file ",GetLastError());
      return(false);
     }
   c_copyright="(C)opyright 2003, MetaQuotes Software Corp.";
   ArrayInitialize(i_unused,0);
//--- write history file header
   FileWriteInteger(HandleHistory,file_version,LONG_VALUE);
   FileWriteString(HandleHistory,c_copyright,64);
   FileWriteString(HandleHistory,c_symbol,12);
   FileWriteInteger(HandleHistory,i_period,LONG_VALUE);
   FileWriteInteger(HandleHistory,i_digits,LONG_VALUE);
   FileWriteInteger(HandleHistory,0,LONG_VALUE);
   FileWriteInteger(HandleHistory,0,LONG_VALUE);
   FileWriteArray(HandleHistory,i_unused,0,13);
   return(true);
  }
//+------------------------------------------------------------------+

Кроме создания самого файла истории "*.hst" и создания заголовка файла (несколько первых служебных строчек), в функции CreateHeader() запоминается хэндл созданного файла в переменную HandleHistory.

1.4. Первая запись истории в *.hst файл

После создания файла *.hst и заполнения его заголовка нужно провести первую запись истории котировок в файл — то есть, нужно заполнить файл всей историей, актуальной на данный момент. За первую запись истории в файл будет отвечать функция FirstWriteHistory() (ее можно просмотреть в индикаторе).

Когда возникает ситуация "первая запись истории" в нашем индикаторе? Логично предположить, что это происходит при первой загрузке индикатора.

Первую загрузку можно (и нужно) контролировать в индикаторе по значению переменной prev_calculated. Значение prev_calculated==0 говорит о том, что происходит именно первая загрузка. Но в то же время prev_calculated==0 может ещё означать, что загрузка не первая, но была подкачана история. Что делать при подкачке истории, мы обговорим при редактировании кода OnCalculate().

1.5. Запись онлайн-котировок

После создания и заполнения заголовка файл *.hst и первой записи истории можно приступать к записи онлайн-котировок. За это отвечает функция CollectTicks() (ее можно просмотреть в индикаторе).

Изменения индикатора, описанные выше, можно увидеть в файле IndCreateOfflineStep2.mq4

1.6. Редактирование функции OnCalculate()

Введём переменную first_start. Она будет хранить значение true после первого старта. Другими словами при first_start==true мы будет знать, что наш индикатор ещё не создавал файл *.hst.

//--- input parameter
input ENUM_OFF_TIMEFRAMES  ExtOffPeriod=M6;
//---
bool     first_start=true;    // true -> it's first start
bool     crash=false;         // false -> error in the code

Алгоритм работы функции OnCalculate() нашего индикатора:

algorithm

Рис. 1. Алгоритм функции OnCalculate() 

Окончательную версию индикатора можно увидеть в файле IndCreateOffline.mq4

 

2. Запуск индикатора на автономном графике

2.1. Обновление автономного графика при помощи ChartSetSymbolPeriod() 

Важное замечание: для обновления автономного графика вместо ChartRedraw() нужно вызывать ChartSetSymbolPeriod() с текущими параметрами. Вызов ChartSetSymbolPeriod() происходит в функции CollectTicks() c интервалом не чаще 1 раза в 3 секунды.

Также нужно учесть один нюанс работы автономного графика: индикатор, прикреплённый на автономный график, при каждом его обновлении будет получать в своей функции OnCalculate() prev_calculated==0. Эту спефицику нужно запомнить. Ниже будет показан метод, который учитывает данную специфику. 

2.2. Режим обслуживания 

Что мы хотим получить: один и тот же индикатор должен работать и на обычном онлайн-графике, и на автономном графике. При этом у индикатора меняется поведение в зависимости от того, на каком графике — онлайн или автономном — он находится. Когда индикатор находится на онлайн графике, то его функционал очень похож на индикатор IndCreateOffline.mq4, который мы рассмотрели выше. А вот в автономном режиме он начинает работать как обычный индикатор.

Итак, назовём наш индикатор IndMACDDoubleDuty — он будет построен на основе рассмотренного выше IndCreateOffline.mq4 и стандартного индикатора MACD.mq4. Подготовьте, пожалуйста, черновик будущего индикатора: в MetaEditor'e откройте файл IndCreateOffline.mq4, далее меню "Файл" -> "Сохранить как..." и впишите название индикатора IndMACDDoubleDuty.mq4

Сразу добавим описание индикатора — наш индикатор теперь создаёт автономные графики и имеет двойное назначение:

//+------------------------------------------------------------------+
//|                                            IndMACDDoubleDuty.mq4 |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property description "The indicator creates an offline chart."
#property description "May work on an online chart and off-line graph."
#property strict
#property indicator_chart_window 

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

Для запоминания, в каком режиме работает индикатор, введём переменную mode_offline:

//--- input parameter
input ENUM_OFF_TIMEFRAMES  ExtOffPeriod=M6;
//---
bool     first_start=true;    // true -> it's first start
bool     crash=false;         // false -> error in the code
bool     mode_offline=true;   // true -> on the offline chart
int      HandleHistory=-1;    // handle for the opened "*.hst" file
datetime time0;               //

Соответственно немного изменится OnInit():

int OnInit()
  {
   mode_offline=IsOffline(ChartID());
   if(!mode_offline && Period()!=PERIOD_M1)
     {
      Print("The period on the online chart must be \"M1\"!");
      crash=true;
     }
//---
   return(INIT_SUCCEEDED);
  }

Внесём изменения в OnCalculate():

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//---
   if(crash)
      return(rates_total);

   if(!mode_offline) // work in the online chart 
     {
      if(prev_calculated==0 && first_start) // first start
        {
         .
         .
         .
         first_start=false;
        }
      //---
      CollectTicks();
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

На данном этапе индикатор, при прикреплении его на онлайн график с периодом PERIOD_M1, переходит в режим "обслуживание" и создаёт автономный график.

Изменения индикатора, описанные выше, можно увидеть в файле IndMACDDoubleDutyStep1.mq4

2.3. Копирование индикатора на автономный график

В индикатор IndMACDDoubleDuty добавим новый функционал: теперь, находясь в режиме "обслуживание", индикатор должен передавать свою копию на созданный автономный график. В этом нам помогут такие функции: ChartSaveTemplate и ChartApplyTemplate. Теперь алгоритм OnCalcalculate() будет выглядеть так:

algorithm_2

Рис. 2. Алгоритм функции OnCalculate()  

Добавим в код OnCalculate() дополнительный функционал:

         else
            Print(__FUNCTION__,"Opening offline chart id=",ChartOffID);
         ResetLastError();
         if(!ChartSaveTemplate(0,"IndMACDDoubleDuty"))
           {
            Print("Error save template: ",GetLastError());
            first_start=false;
            crash=true;
            return(rates_total);
           }
         ResetLastError();
         if(!ChartApplyTemplate(ChartOffID,"IndMACDDoubleDuty"))
           {
            Print("Error apply template: ",GetLastError());
            first_start=false;
            crash=true;
            return(rates_total);
           }
        }
      //---
      if(prev_calculated==0 && !first_start) // a deeper history downloaded or history blanks filled

Теперь наш индикатор, при прикреплении его на онлайн график с периодом PERIOD_M1, переходит в режим "обслуживание", создаёт автономный график и копирует себя на него.

Изменения индикатора, описанные выше, можно увидеть в файле IndMACDDoubleDutyStep2.mq4.  

2.4. Интеграция в MACD.mq4

Наш индикатор уже размещает сам себя на автономном графике, но пока ничего не отображает и ничего не считает. Исправим это: интегрируем наш индикатор в стандартный MACD.mq4

Сначала вставим входные параметры индикатора MACD.mq4 в наш код:

#property strict

#include <MovingAverages.mqh>

//--- MACD indicator settings
#property  indicator_separate_window
#property  indicator_buffers 2
#property  indicator_color1  Silver
#property  indicator_color2  Red
#property  indicator_width1  2
//--- indicator parameters
input int InpFastEMA=12;   // Fast EMA Period
input int InpSlowEMA=26;   // Slow EMA Period
input int InpSignalSMA=9;  // Signal SMA Period
//--- indicator buffers
double    ExtMacdBuffer[];
double    ExtSignalBuffer[];
//--- right input parameters flag
bool      ExtParameters=false;
//+------------------------------------------------------------------+
//| Enumerations of periods offline chart                            |
//+------------------------------------------------------------------+ 

Затем добавим код в OnInit(). Здесь нужно отметить, что инициализация параметров индикатора MACD должна проходить при любых условиях — и на автономном, и на онлайн-графике, причем даже в том случае, когда период онлайн-графика отличается от PERIOD_M1:

int OnInit()
  {
   mode_offline=IsOffline(ChartID());
   if(!mode_offline && Period()!=PERIOD_M1)
     {
      Print("The period on the online chart must be \"M1\"!");
      crash=true;
     }
//--- init MACD indicator
   IndicatorDigits(Digits+1);
//--- drawing settings
   SetIndexStyle(0,DRAW_HISTOGRAM);
   SetIndexStyle(1,DRAW_LINE);
   SetIndexDrawBegin(1,InpSignalSMA);
//--- indicator buffers mapping
   SetIndexBuffer(0,ExtMacdBuffer);
   SetIndexBuffer(1,ExtSignalBuffer);
//--- name for DataWindow and indicator subwindow label
   IndicatorShortName("MACD("+IntegerToString(InpFastEMA)+","+IntegerToString(InpSlowEMA)+","+IntegerToString(InpSignalSMA)+")");
   SetIndexLabel(0,"MACD");
   SetIndexLabel(1,"Signal");
//--- check for input parameters
   if(InpFastEMA<=1 || InpSlowEMA<=1 || InpSignalSMA<=1 || InpFastEMA>=InpSlowEMA)
     {
      Print("Wrong input parameters");
      ExtParameters=false;
      return(INIT_FAILED);
     }
   else
      ExtParameters=true;
//---
   return(INIT_SUCCEEDED);
  }

Следующим шагом немного изменим код в начале OnCalculate(). Если мы находимся на онлайн-графике и его период не равен PERIOD_M1 мы должны дать возможность для расчёта параметров MACD. Было так:

const long &volume[],
                const int &spread[])
  {
//---
   if(crash)
      return(rates_total);

   if(!mode_offline) // work in the online chart 
     {
      if(prev_calculated==0 && first_start) // first start
        {

 а станет так:

const long &volume[],
                const int &spread[])
  {
//---
   if(!mode_offline && !crash) // work in the online chart 
     {
      if(prev_calculated==0 && first_start) // first start
        {

Дальше добавим код расчёта параметров индикатора MACD в конец OnCalculate():

         FirstWriteHistory(ExtOffPeriod);
         first_start=false;
        }
      //---
      CollectTicks();
     }
//---
   int i,limit;
//---
   if(rates_total<=InpSignalSMA || !ExtParameters)
      return(0);
//--- last counted bar will be recounted
   limit=rates_total-prev_calculated;
   if(prev_calculated>0)
      limit++;
//--- macd counted in the 1-st buffer
   for(i=0; i<limit; i++)
      ExtMacdBuffer[i]=iMA(NULL,0,InpFastEMA,0,MODE_EMA,PRICE_CLOSE,i)-
                       iMA(NULL,0,InpSlowEMA,0,MODE_EMA,PRICE_CLOSE,i);
//--- signal line counted in the 2-nd buffer
   SimpleMAOnBuffer(rates_total,prev_calculated,0,InpSignalSMA,ExtMacdBuffer,ExtSignalBuffer);
//--- return value of prev_calculated for next call
   return(rates_total);
  }

2.5. Экономный пересчёт индикатора на автономном графике

Напомню нюанс, описанный в начале раздела 2:

Также нужно учесть один нюанс работы автономного графика: индикатор, прикреплённый на автономный график, при каждом его обновлении будет получать в своей функции OnCalculate() prev_calculated==0. Эту спефицику нужно запомнить. Ниже будет показан метод, который учитывает данную специфику. 

И еще одно напоминание: о том, что в OnCalculate() значение prev_calculated==0 может означать две ситуации:

  1. или это первый запуск индикатора;
  2. или была подгружена история.

В обоих случаях индикатор должен пересчитать все бары на графике. Когда это нужно выполнить только один раз (при загрузке) — это нормально. А вот на автономном графике мы будем получать prev_calculated==0 при каждом обновлении (примерно раз в 2-3 секунды), и индикатор будет пересчитывать все бары. Это очень неэкономный расход ресурсов. Поэтому применим небольшую хитрость: когда индикатор находится на автономном графике, он будет хранить и сравнивать количество баров (переменная rates_total) и время самого правого бара на графике.

Шаг 1: В начале OnCalculate() объявим две статические переменные и одну псевдо-переменную:

                const long &volume[],
                const int &spread[])
  {
//---
   static int static_rates_total=0;
   static datetime static_time_close=0;
   int pseudo_prev_calculated=prev_calculated;
//---
   if(!mode_offline && !crash) // work in the online chart 
     {

Шаг 2: В блоке кода расчёта значений индикатора заменим переменную prev_calculated на pseudo_prev_calculated:

      CollectTicks();
     }
//---
   int i,limit;
//---
   if(rates_total<=InpSignalSMA || !ExtParameters)
      return(0);
//--- last counted bar will be recounted
   limit=rates_total-pseudo_prev_calculated;
   if(pseudo_prev_calculated>0)
      limit++;
//--- macd counted in the 1-st buffer
   for(i=0; i<limit; i++)
      ExtMacdBuffer[i]=iMA(NULL,0,InpFastEMA,0,MODE_EMA,PRICE_CLOSE,i)-
                       iMA(NULL,0,InpSlowEMA,0,MODE_EMA,PRICE_CLOSE,i);
//--- signal line counted in the 2-nd buffer
   SimpleMAOnBuffer(rates_total,pseudo_prev_calculated,0,InpSignalSMA,ExtMacdBuffer,ExtSignalBuffer);
//---
   if(mode_offline) // work in the offline chart 

Шаг 3: Здесь будем вычислять значение для псевдо-переменной, если индикатор работает на автономном графике. 

      CollectTicks();
     }
//---
   if(mode_offline) // work in the offline chart 
     {
      if(time[0]>static_time_close) // new bar
        {
         if(static_time_close==0)
            pseudo_prev_calculated=0;
         else // search bar at which time[0]==static_time_close
           {
            for(int i=0;i<rates_total;i++)
              {
               if(time[i]==static_time_close)
                 {
                  pseudo_prev_calculated=rates_total-i;
                  break;
                 }
              }
           }
        }
      else
        {
         pseudo_prev_calculated=rates_total;
        }
      //---
      static_rates_total=rates_total;
      static_time_close=time[0];
     }
//---
   int i,limit;

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

2.6. Подкачка истории. Автономный график

Осталось решить, что делать, если на онлайн-графике с нашим индикатором произошла подкачка истории (при этом prev_calculated==0). Я предлагаю решать такую ситуацию через глобальные переменные терминала. Алгоритм работы следующий: если на онлайн-графике получаем prev_calculated==0 (неважно, произошел ли первый запуск или просто подкачка истории), мы просто создаём глобальную переменную. Индикатор на автономном графике при каждом обновлении проверяет наличие глобальной переменной: если она есть (значит, на онлайн графике было prev_calculated==0), то индикатор будет пересчитан полностью и удалит глобальную переменную.

Добавим в шапку индикатора переменную, в которой будем хранить имя будущей глобальной переменной терминала, и в OnInit() генерируем это имя:

MqlRates rate;                //
long     ChartOffID=-1;       // ID of the offline chart
string   NameGlVariable="";   // name global variable
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   mode_offline=IsOffline(ChartID());
   if(!mode_offline && Period()!=PERIOD_M1)
     {
      Print("The period on the online chart must be \"M1\"!");
      crash=true;
     }
   NameGlVariable=Symbol()+(string)ExtOffPeriod;
//--- init MACD indicator
   IndicatorDigits(Digits+1);
//--- drawing settings

Добавим код создания глобальной переменной терминала, если выполняется условие prev_calculated==0 и индикатор находится на онлайн-графике:

         if(!ChartApplyTemplate(ChartOffID,"IndMACDDoubleDuty"))
           {
            Print("Error apply template: ",GetLastError());
            first_start=false;
            crash=true;
            return(rates_total);
           }
         //---
         ResetLastError();
         if(GlobalVariableSet(NameGlVariable,0.0)==0) // creates a new global variable
           {
            Print("Failed to creates a new global variable ",GetLastError());
           }
        }
      //---
      if(prev_calculated==0 && !first_start) // a deeper history downloaded or history blanks filled
        {
         Print("a deeper history downloaded or history blanks filled. first_start=",first_start);
         if(CreateHeader(ExtOffPeriod))
            first_start=false;
         else
           {
            crash=true;
            return(rates_total);
           }
         //---
         FirstWriteHistory(ExtOffPeriod);
         first_start=false;
         //---
         ResetLastError();
         if(GlobalVariableSet(NameGlVariable,0.0)==0) // creates a new global variable
           {
            Print("Failed to creates a new global variable ",GetLastError());
           }
        }
      //---
      CollectTicks();

И последнее изменение: проверка наличия глобальной переменной из индикатора на автономном графике:

      else
        {
         pseudo_prev_calculated=rates_total;
        }
      //---
      if(GlobalVariableCheck(NameGlVariable))
        {
         pseudo_prev_calculated=0;
         GlobalVariableDel(NameGlVariable);
        }
      //---
      Print("rates_total=",rates_total,"; prev_calculated=",
            prev_calculated,"; pseudo_prev_calculated=",pseudo_prev_calculated);
      static_rates_total=rates_total;
      static_time_close=time[0];
     }
//---
   int i,limit;

Окончательная версия индикатора, с последними изменениями: IndMACDDoubleDuty.mq4.  

 

3. Индикатор, отображающий Renko-бары

Renko-бары мы будем строить по той же технологии, что использовалась в пунктах 1. Индикатор "IndCreateOffline", который будет создавать автономный график  и 2. Запуск индикатора на автономном графике. То есть, в итоге мы получим автономный график, но только в данном случае файл истории *.hst будет содержать бары одинакового размера — их ещё называют "кирпичами". Размер "кирпичей" задаётся в настройках индикатора и измеряется в пунктах.

Renko-бары  

Рис. 3. Renko-бары 

Перед началом построения нужно учитывать несколько правил формирования файла истории *.hst для построения Renko-баров.

3.1. Правила построения Range-баров

Правило1: запись OHLC в файл истории *.hst должна быть корректная, особенно это касается значений High и Low. В противном случае терминал не отобразит некорректную запись. Пример корректной и некорректной записи в файл истории *.hst бычьего бара:

Пример корректной и некорректной записи  

Рис. 4. Пример корректной и некорректной записи 

Правило 2: хоть мы и строим Range-бары, которые не имеют временной привязки, но сам формат файла истории *.hst требует наличия параметра time — это время начала периода. Поэтому параметр time должен обязательно записываться.

Правило 3: параметр time у всех баров должен отличаться. Если записать для всех баров одинаковый параметр time, то такая запись будет некорректной, и терминал просто не отобразит график. Но есть тут и приятная особенность: параметр time допустимо записывать с разницей в 1 секунду. Например, один бар запишем с параметром time=2016.02.10 09:08:00, а следующий — с параметром  time=2016.02.10 09:08:01.

3.2. Формирование "кирпичей" на исторических данных

Данный метод не претендует на звание совершенного алгоритма. Я выбрал упрощенный подход, поскольку главная задача этой статьи — показать, как формировать файл истории *.hst. В индикаторе "IndRange.mq4", при рисовании кирпичей на основании истории, анализируются значения High[i] и Low[i] текущего периода. То есть, если индикатор "IndRange.mq4" прикрепить на график с периодом M5, то индикатор "IndRange.mq4" будет при первом запуске анализировать историю по текущему периоду M5. 

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

Предыдущий кирпич - бычий

Рис. 5. Предыдущий кирпич — бычий     


 Предыдущий кирпич - медвежий

Рис. 6. Предыдущий кирпич — медвежий

Интересный нюанс: координаты баров (Open, High, Low, Close, Time и объёмы) хранятся в структуре rate, которая объявлена в "шапке" индикатора

MqlRates rate;                   //

и эта структура при каждой записи в файл истории *.hst не обнуляется, а просто переписывается. Благодаря этому легко реализовать алгоритмы, которые представлены на рис. 5 и на рис. 6., и можно дать общую формулу:

Open = High; Low = Open; Close = Low + Renko size; High = Close

 

Low = OpenHigh = OpenClose = High - Renko sizeLow = Close

А вот так эти формулы выглядят в индикаторе "IndRange.mq4", в функции FirstWriteHistory():

//+------------------------------------------------------------------+
//| First Write History                                              |
//+------------------------------------------------------------------+
bool FirstWriteHistory(const int offline_period)
  {
   int      i,start_pos;
   int      cnt=0;
//--- write history file
   periodseconds=offline_period*60;
   start_pos=Bars-1;
   rate.open=Open[start_pos];
   rate.close=Close[start_pos];
   rate.low=Low[start_pos];
   rate.high=High[start_pos];
   rate.tick_volume=(long)Volume[start_pos];
   rate.spread=0;
   rate.real_volume=0;
//--- normalize open time
   rate.time=D'1980.07.19 12:30:27';
   for(i=start_pos-1; i>=0; i--)
     {
      if(IsStopped())
         break;
      while((High[i]-rate.high)>SizeRenko*Point())
        {
         rate.time+=1;
         rate.open=rate.high;
         rate.low=rate.open;
         rate.close=NormalizeDouble(rate.low+SizeRenko*Point(),Digits);
         rate.high=rate.close;
         last_fpos=FileTell(HandleHistory);
         uint byteswritten=FileWriteStruct(HandleHistory,rate);
         //--- check the number of bytes written 
         if(byteswritten==0)
            PrintFormat("Error read data. Error code=%d",GetLastError());
         else
            cnt++;
        }
      while((Low[i]-rate.low)<-SizeRenko*Point())
        {
         rate.time+=1;
         rate.open=rate.low;
         rate.high=rate.open;
         rate.close=NormalizeDouble(rate.high-SizeRenko*Point(),Digits);
         rate.low=rate.close;
         last_fpos=FileTell(HandleHistory);
         uint byteswritten=FileWriteStruct(HandleHistory,rate);
         //--- check the number of bytes written 
         if(byteswritten==0)
            PrintFormat("Error read data. Error code=%d",GetLastError());
         else
            cnt++;
        }
     }
   FileFlush(HandleHistory);
   PrintFormat("%d record(s) written",cnt);
   return(true);
  }

 

Индикатор "IndRange.mq4" всегда формирует название файла истории *.hst по следующему правилу: "Имя текущего символа"+"7"+".hst". Например для символа "EURUSD" файл истории будет иметь имя "EURUSD7.hst".

3.3. Работа индикатора онлайн

При работе индикатора онлайн нужно вместо цены High[i] анализировать цену Close[0] — цену закрытия на нулевом (самом правом) баре:

//+------------------------------------------------------------------+
//| Collect Ticks                                                    |
//+------------------------------------------------------------------+
bool CollectTicks()
  {
   static datetime last_time;//=TimeLocal()-5;
   long     chart_id=0;
   datetime cur_time=TimeLocal();
//---
   while((Close[0]-rate.high)>SizeRenko*Point())
     {
      rate.time+=1;
      rate.open=rate.high;
      rate.low=rate.open;
      rate.close=NormalizeDouble(rate.low+SizeRenko*Point(),Digits);
      rate.high=rate.close;
      last_fpos=FileTell(HandleHistory);
      uint byteswritten=FileWriteStruct(HandleHistory,rate);
      //--- check the number of bytes written  
      if(byteswritten==0)
         PrintFormat("Error read data. Error code=%d",GetLastError());
     }
   while((Close[0]-rate.low)<-SizeRenko*Point())
     {
      rate.time+=1;
      rate.open=rate.low;
      rate.high=rate.open;
      rate.close=NormalizeDouble(rate.high-SizeRenko*Point(),Digits);
      rate.low=rate.close;
      last_fpos=FileTell(HandleHistory);
      uint byteswritten=FileWriteStruct(HandleHistory,rate);
      //--- check the number of bytes written 
      if(byteswritten==0)
         PrintFormat("Error read data. Error code=%d",GetLastError());
     }
//--- refresh window not frequently than 1 time in 2 seconds
   if(cur_time-last_time>=3)
     {
      FileFlush(HandleHistory);
      ChartSetSymbolPeriod(ChartOffID,Symbol(),i_period);
      last_time=cur_time;
     }
   return(true);
  }


4. Индикатор, который будет создавать нестандартный символ — индекс доллара USDx

Важные замечания к алгоритму  индикатора "IndUSDx.mq4": индикатор индекса доллара не претендует на абсолютную верность формулы расчёта индекса, ведь для нас сейчас главное — показать, как формировать файл истории *.hst по нестандартному символу. В итоге будет получен автономный график, а бары этого графика будут отображать рассчитанный индекс доллара. Также индикатор "IndUSDx.mq4" создаёт автономный график только один раз: или при первом присоединении индикатора к графику, или после смены периода графика.

Формула для расчёта индекса доллара и набор символов взяты на основании кода: Простой индикатор индекса доллара

Для формулы расчёта индекса доллара нужно получить данные Time, Open и Close по символам "EURUSD", "GBPUSD", "USDCHF", "USDJPY", "AUDUSD", "USDCAD" и "NZDUSD". Для удобства сохранения и обращения к данным введена структура OHLS (она объявлена в "шапке" индикатора):

//--- structuts
struct   OHLS
  {
   datetime          ohls_time[];
   double            ohls_open[];
   double            ohls_close[];
  };

В структуре OHLS в качестве элементов выступают массивы для хранения Time, Open и Close. Под описанием структуры сразу объявлены несколько объектов — структур OHLS, в которых будут хранится данные расчётных символов:

//--- structuts
struct   OHLS
  {
   datetime          ohls_time[];
   double            ohls_open[];
   double            ohls_close[];
  };
OHLS     OHLS_EURUSD,OHLS_GBPUSD,OHLS_USDCHF,OHLS_USDJPY,OHLS_AUDUSD,OHLS_USDCAD,OHLS_NZDUSD;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()

В OnInit() происходит вызов функции SelectSymbols():

//+------------------------------------------------------------------+
//| Select Symbols                                                   |
//+------------------------------------------------------------------+
bool SelectSymbols()
  {
   bool rezult=true;
   string arr_symbols[7]={"EURUSD","GBPUSD","USDCHF","USDJPY","AUDUSD","USDCAD","NZDUSD"};
   for(int i=0;i<ArraySize(arr_symbols);i++)
      rezult+=SymbolSelect(arr_symbols[i],true);
//---
   return(rezult);
  }

Функция SelectSymbols(), при помощи SymbolSelect, выбирает символы, которые участвуют в формуле индекса доллара, в окне MarketWatch.

В OnCalculate() при первом запуске вызывается функция CopyCloseSymbols(). Здесь происходит запрос данных по расчётным символам и заполнение структур символов:

//+------------------------------------------------------------------+
//| CopyClose Symbols                                                |
//+------------------------------------------------------------------+
bool CopyCloseSymbols(const int rates)
  {
   int copied=0;
   int copy_time=0,copy_open=0,copy_close=0;
   copy_time=CopyTime("EURUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("EURUSD",Period(),0,rates,OHLS_EURUSD.ohls_open);
   copy_close=CopyClose("EURUSD",Period(),0,rates,OHLS_EURUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"EURUSD\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }

   copy_time=CopyTime("GBPUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("GBPUSD",Period(),0,rates,OHLS_GBPUSD.ohls_open);
   copy_close=CopyClose("GBPUSD",Period(),0,rates,OHLS_GBPUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"GBPUSD\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }

   copy_time=CopyTime("USDCHF",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("USDCHF",Period(),0,rates,OHLS_USDCHF.ohls_open);
   copy_close=CopyClose("USDCHF",Period(),0,rates,OHLS_USDCHF.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"USDCHF\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }

   copy_time=CopyTime("USDJPY",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("USDJPY",Period(),0,rates,OHLS_USDJPY.ohls_open);
   copy_close=CopyClose("USDJPY",Period(),0,rates,OHLS_USDJPY.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"USDJPY\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }

   copy_time=CopyTime("AUDUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("AUDUSD",Period(),0,rates,OHLS_AUDUSD.ohls_open);
   copy_close=CopyClose("AUDUSD",Period(),0,rates,OHLS_AUDUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"AUDUSD\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }

   copy_time=CopyTime("USDCAD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("USDCAD",Period(),0,rates,OHLS_USDCAD.ohls_open);
   copy_close=CopyClose("USDCAD",Period(),0,rates,OHLS_USDCAD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"USDCAD\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }

   copy_time=CopyTime("NZDUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("NZDUSD",Period(),0,rates,OHLS_NZDUSD.ohls_open);
   copy_close=CopyClose("NZDUSD",Period(),0,rates,OHLS_NZDUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("Symbol \"NZDUSD\". Get time ",copy_time,", get open ",copy_open,", get close ",copy_close," of ",rates);
      return(false);
     }
//---
   return(true);
  }

Если в функции CopyCloseSymbols() история по символу скачана меньше, чем заданное значение, то выводится сообщение с названием символа и фактическим значением скачанной истории по символу.

В случае удачного заполнения структур произойдет вызов основного функционала — функции FirstWriteHistory(), которая наполняет историей файл *.hst:

//+------------------------------------------------------------------+
//| First Write History                                              |
//+------------------------------------------------------------------+
bool FirstWriteHistory(const int rates)
  {
   int      i;
   int      cnt=0;
   rate.tick_volume=0;
   rate.spread=0;
   rate.real_volume=0;
   for(i=0;i<rates;i++)
     {
      rate.time=OHLS_EURUSD.ohls_time[i];
      rate.open=(100*MathPow(OHLS_EURUSD.ohls_open[i],0.125)+100*MathPow(OHLS_GBPUSD.ohls_open[i],0.125)+
                 100*MathPow(OHLS_USDCHF.ohls_open[i],0.125)+100*MathPow(OHLS_USDJPY.ohls_open[i],0.125)+
                 100*MathPow(OHLS_AUDUSD.ohls_open[i],0.125)+100*MathPow(OHLS_USDCAD.ohls_open[i],0.125)+
                 100*MathPow(OHLS_NZDUSD.ohls_open[i],0.125))/8.0;

      rate.close=(100*MathPow(OHLS_EURUSD.ohls_close[i],0.125)+100*MathPow(OHLS_GBPUSD.ohls_close[i],0.125)+
                  100*MathPow(OHLS_USDCHF.ohls_close[i],0.125)+100*MathPow(OHLS_USDJPY.ohls_close[i],0.125)+
                  100*MathPow(OHLS_AUDUSD.ohls_close[i],0.125)+100*MathPow(OHLS_USDCAD.ohls_close[i],0.125)+
                  100*MathPow(OHLS_NZDUSD.ohls_close[i],0.125))/8.0;

      if(rate.open>rate.close)
        {
         rate.high=rate.open;
         rate.low=rate.close;
        }
      else
        {
         rate.high=rate.close;
         rate.low=rate.open;
        }
      last_fpos=FileTell(HandleHistory);
      uint byteswritten=FileWriteStruct(HandleHistory,rate);
      //--- check the number of bytes written 
      if(byteswritten==0)
         PrintFormat("Error read data. Error code=%d",GetLastError());
      else
         cnt++;
     }
   FileFlush(HandleHistory);
   PrintFormat("%d record(s) written",cnt);
   return(true);
  }

Результат работы индикатора "IndUSDx.mq4": 

 Индикатор "IndUSDx.mq4

Рис. 7. Индикатор "IndUSDx.mq4 

  

Заключение

Оказалось, что и на автономных графиках, так же, как и на онлайн-графиках, можно осуществлять экономный пересчет индикаторов. Правда, для этого нужно вносить изменения в код индикатора, для учета специфики обновления автономного графика, а именно — учитывать, что при обновлении автономного графика все индикаторы получают в OnCalculate() значение prev_calculate==0.

Также в статье было показано, как формировать файл истории *.hst и за счет этого коренным образом менять параметры отображаемых баров на графике.