Как написать для Маркета индикатор любых нестандартных графиков
Оглавление
- Введение
- 1. Индикатор "IndCreateOffline", который будет создавать автономный график
- 1.1. Редактирование "шапки" индикатора
- 1.2. Контроль типа графика
- 1.3. Функция создания заголовка файла истории
- 1.4. Первая запись истории в *.hst файл
- 1.5. Запись онлайн котировок
- 1.6. Редактирование функции OnCalculate()
- 2. Запуск индикатора на автономном графике
- 2.1. Обновление автономного графика при помощи ChartSetSymbolPeriod()
- 2.2. Режим обслуживания
- 2.3. Копирование индикатора на автономный график
- 2.4. Интеграция в MACD.mq4
- 2.5. Экономный пересчёт индикатора на автономном графике
- 2.6. Подкачка истории. Автономный график
- 3. Индикатор отображающий Renko-бары
- 3.1. Правила построения Range-баров
- 3.2. Формирование "кирпичей" на исторических данных
- 3.3. Работа индикатора онлайн
- 4. Индикатор, который будет создавать нестандартный символ - индекс доллара USDx
От японских свечей до графиков Ренко
На сегодняшний день самый популярный вид графиков среди трейдеров — японские
свечи, которые помогают легко оценить текущую ситуацию на рынке. Свечные графики
дают хорошее и наглядное представление о развитии цены в течение промежутка
времени, охватываемого одной свечой. Но некоторые трейдеры считают недостатком
то, что графики несут в себе компонент времени, и предпочитают иметь дело только
с изменением цены. Так появились графики "Крестики-Нолики", "Ренко", "Каги", "Range
bars", эквиобъемные графики и т.п.
С помощью оффлайновых графиков, программирования на языке MQL4 и небольшого
опыта все эти графики вы можете получить в MetaTrader 4. Есть возможность создавать
графики с собственными синтетическими инструментами (которых нет у брокера
или даже вовсе в природе) и нестандартными таймфреймами, которых нет в платформе.
Большинство разработчиков используют для этого вызовы DLL и сложные схемы.
В этой статье мы расскажем, как создать индикаторы вида "два в одном" любой
сложности, которые не только не требуют знаний DLL, но и могут быть легко
опубликованы в Маркете в виде продукта, поскольку являются полностью независимыми
и законченными приложениями.
Примеры из этой статьи вы сможете найти и скачать в виде бесплатных приложений Маркета:
- USDx Chart MT4 - индикатор "USDx Chart MT4" строит автономный график, на котором вместо привычных баров и свечей рисуется индекс доллара.
- Renko Chart MT4 - индикатор Renko Chart MT4 создает автономный график Renko, на котором все бары имеют вид Renko "кирпичей". Сами "кирпичи" не имеют теней, а размер "кирпича" задается в настройках.
Индикатору, который создаёт автономный график нестандартного символа и/или периода нет необходимости вызывать 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 |
Наш индикатор 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().
После создания и заполнения заголовка файл *.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() нашего индикатора:
Рис. 1. Алгоритм функции OnCalculate()
Окончательную версию индикатора можно увидеть в файле IndCreateOffline.mq4.
2. Запуск индикатора на автономном графике
2.1. Обновление автономного графика при помощи ChartSetSymbolPeriod()
Важное замечание: для обновления автономного графика вместо ChartRedraw() нужно вызывать ChartSetSymbolPeriod() с текущими параметрами. Вызов ChartSetSymbolPeriod() происходит в функции CollectTicks() c интервалом не чаще 1 раза в 3 секунды.
Также нужно учесть один нюанс работы автономного графика: индикатор, прикреплённый на автономный график, при каждом его обновлении будет получать в своей функции OnCalculate() prev_calculated==0. Эту спефицику нужно запомнить. Ниже будет показан метод, который учитывает данную специфику.
Что мы хотим получить: один и тот же индикатор должен работать и на обычном онлайн-графике, и на автономном графике. При этом у индикатора меняется поведение в зависимости от того, на каком графике — онлайн или автономном — он находится. Когда индикатор находится на онлайн графике, то его функционал очень похож на индикатор 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. В этом режиме он выполняет следующие функции:
- создаёт *.hst файл и заполняет его;
- открывает автономный график на базе *.hst файла;
- при поступлении котировок дописывает историю в *.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 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() будет выглядеть так:
Рис. 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.
Наш индикатор уже размещает сам себя на автономном графике, но пока ничего не отображает и ничего не считает. Исправим это: интегрируем наш индикатор в стандартный 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 может означать две ситуации:
- или это первый запуск индикатора;
- или была подгружена история.
В обоих случаях индикатор должен пересчитать все бары на графике. Когда это нужно выполнить только один раз (при загрузке) — это нормально. А вот на автономном графике мы будем получать 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 будет содержать бары одинакового размера — их ещё называют "кирпичами". Размер "кирпичей" задаётся в настройках индикатора и измеряется в пунктах.
Рис. 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. Общая схема работы:
- если High[i] больше, чем high предыдущего кирпича на размер кирпича, то
выше предыдущего кирпича рисуется один или более кирпичей;
- если Low[i] меньше чем low предыдущего кирпича на размер кирпича, то выше предыдущего кирпича рисуется один или более кирпичей.
Рис. 5. Предыдущий кирпич — бычий
Рис. 6. Предыдущий кирпич — медвежий
Интересный нюанс: координаты баров (Open, High, Low, Close, Time и объёмы) хранятся в структуре rate, которая объявлена в "шапке" индикатора
MqlRates rate; //
и эта структура при каждой записи в файл истории *.hst не обнуляется, а просто переписывается. Благодаря этому легко реализовать алгоритмы, которые представлены на рис. 5 и на рис. 6., и можно дать общую формулу:
- когда High[i] больше чем high предыдущего кирпича (при этом неважно, бычьим или медвежьим был предыдущий кирпич) на размер кирпича, координаты для нового бара вычисляются по такой схеме:
Open = High; Low = Open; Close = Low + Renko size; High = Close
- когда Low[i] меньше чем low предыдущего кирпича (при этом неважно, бычьим или медвежьим был предыдущий кирпич) на размер кирпича, координаты для нового бара вычисляются по такой схеме:
Low = Open; High = Open; Close = High - Renko size; Low = 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".
При работе индикатора онлайн нужно вместо цены 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":
Рис. 7. Индикатор "IndUSDx.mq4
Заключение
Оказалось, что и на автономных графиках, так же, как и на онлайн-графиках, можно осуществлять экономный пересчет индикаторов. Правда, для этого нужно вносить изменения в код индикатора, для учета специфики обновления автономного графика, а именно — учитывать, что при обновлении автономного графика все индикаторы получают в OnCalculate() значение prev_calculate==0.
Также в статье было показано, как формировать файл истории *.hst и за счет этого коренным образом менять параметры отображаемых баров на графике.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Большая работа проделана. Полезный результат.
Спасибо и Удачи.
Очень понравилась Ваша статья. Есть задача тестирования стратегии. Во время работы индикатора "indcreateoffline", установленного в окне тестера МТ4 (окно visual), который формирует нестандартный ТФ, например М6, вылетает ошибка "indcreateoffline GBPUSD,M1: Error open GBPUSD6.hst file 4059" - Функция не разрешена в тестовом режиме. Есть какие-либо варианты обойти эту проблему? Спасибо.
Никаких вариантов.