Любая торговая система состоит из сигналов открытия и закрытия позиций, подтягивания защитных стопов. Большинство экспертов пишутся на основе стандартных индикаторов, входящих в состав МТ4, которые в справке называются техническими индикаторами. Рассмотрим простейшую систему, основанную на пересечении двух скользящих средних (Moving Average). Этот индикатор имеет два параметра — период усреднения(количество баров, используемых для расчета) и метод усреднения (тип цены). Есть еще и третий параметр — смещение, но мы его в данном примере использовать не будем. Ситуация, когда средняя с коротким периодом пересекает среднюю с длинным периодом, обычно используется для продажи, для покупки все наоборот. Сделаем скрипт (CrossMASignals. mq4), который будет наносить на график сигналы покупок в виде синих стрелок, направленных вверх, и сигналы продаж в виде красных стрелок, направленных вниз.
На рисунке видно, что 21 июля 2005 года средняя с периодом 13 (значение 1.2052) пересекла снизу вверх среднюю с периодом 21 (значение 1.2050). Синюю стрелку мы поставим по цене открытия дневного бара 22.07.2005, так как сигналы мы будем принимать по закрывшимся барам, это избавит от ложных сигналов, когда цена может в течение дня несколько раз сходить вверх и вниз. Создадим такую блок-схему:
Хотя проверять пересечение средних в самом начале графика (от бара с индексом Bars-1) и не совсем правильно, но в данном случае ошибку мы не получим и не будем пока разбирать — почему в начале графика значения средних равны нулю.
Нам необходимо в своем коде получить значения этих средних на каждом баре. Зададим две переменные maLong и maShort и будем присваивать им соответствующие значения. Из справки видно, что необходимо указать символ (NULL), тайм-фрейм (0), значения периодов (21 и 13), смещение (у нас будет нулевым), тип усреднения (простое), тип цены (применим цены закрытия) и индекс бара.
Факт пересечения двух средних можно проверить таким образом — на текущем баре быстрая средняя выше медленной средней, а на предыдущем баре наоборот- медленная средняя выше быстрой средней. Поэтому сделаем не две, а четыре переменные maLongCur, maLongPrev, maShortCur, maShortPrev.
Я сделал рисунок, который показывает связь между параметрами индикатора на графике и параметрами технического индикатора, вызываемого в коде. Для любого встроенного индикатора в МТ4 существует его вызов в языке MQL-4 в виде технического индикатора.
Скрипт почти готов, осталось только как-то поставить в нужных местах стрелки. Тут нам на помощь придут функции для работы с графическими объектами. Чтобы создать стрелку, необходимо: 1. создать Объект типа символ 2. задать координаты времени (X) и цены (Y) , к которым привязывается объект 3. указать вид символа и цвет, которым он будет отображаться на графике Все графические объекты в МТ4 создаются функцией ObjectCreate() . При этом объекту при создании сразу дается уникальное имя, двух объектов с одинаковым именем быть не может. При попытке создать второй объект с уже существующим именем объект создан не будет, ошибка в терминал выведена не будет (но сам факт ошибки проверить можно будет ) чтобы не мешать пользователю. Это самая частая ошибка при работе с графическими объектами, складывается впечатление, что программа не работает, начинаются бесплодные попытки найти ошибку в алгоритме кода, а ее нет — есть ошибка создания множества графических объектов с одинаковыми именами. Мы создаем стрелку функцией ObjectCreate(«уникальное_имя_объекта», OBJ_ARROW, handle_окна, время, цена). Параметр OBJ_ARROW — указывает, что создается объект типа стрелка(символ), handle_окна — указатель на окно, в котором объект будет создан (обычно равен нулю, что указывает на «родное» окно), время и цена — координаты стрелки. После создания стрелки нам необходимо указать код символа — ObjectSet(«уникальное_имя_объекта», OBJPROP_ARROWCODE, код_символа) . Функция ObjectSet() служит для установки свойств графического объекта, для каждого типа объекта требуется свой второй тип параметра, в нашем случае для объекта OBJ_ARROW установить код символа мы можем параметром OBJPROP_ARROWCODE, коды символов можно посмотреть в справке MetaEditor. Кроме того, можно использовать символы из шрифта Wingdings, справка по ним тоже приведена.
Нам подойдут символы с кодом 241 и 242. Далее осталось только задать последнее свойство — цвет символа. Опять используем функцию ObjectSet() и зададим свойство OBJPROP_COLOR либо Red либо Blue по ситуации.
Скрипт готов, но остался последний вопрос — уникальность имени для каждой стрелки. В коде есть объявление строковой переменной arrowName, это имя используется при создании каждой новой стрелки и при изменении атрибутов объектов-стрелок. Самый простой способ решить эту проблему — использовать переменную целого типа, например int arrowCounter, и при каждом создании новой стрелки стрелки увеличивать счетчик стрелок на единицу с помощью arrowCounter++. Уникальное имя будем получать сложением выражения «arrow» и arrowCounter, будем получать имена типа arrow1, arrow2 и так далее.
Скрипт почти окончательно готов, он работает, запускаем на графике EURUSD D1 и мгновенно появляются стрелки. Нажимаем кнопки Ctrl+B — появляется окно со списком графических объектов, прикрепленных к графику.
Мы получили «графический» индикатор, его достоинство — стрелки остаются на своих местах при смене тайм-фрейма, например, при переключении на Weekly. Если мы нажмем кнопку «Правка», то попадем в окно свойств выделенного объекта. Я выбрал стрелку(объект Arrow) с именем arrow1. На вкладках «Общие» и «Параметры» мы видим те значения, что были заданы скриптом — имя объекта, цвет, координаты время и цена, а также код символа — 242.
Единственный неучтенный момент — если мы попробуем запустить скрипт второй раз — он или не сработает или сработает не совсем правильно. Ведь при каждом запуске скрипта создаются объекты с именами arrow1, arrow2 и так далее, но ведь эти объекты уже существуют после запуска скрипта в первый раз. И поэтому, если нам будет необходимо его запустить на том же инструменте, но на другом тайм-фрейме, нам придется вручную предварительно удалить все стрелки, это можно сделать с помощью меню «Графики» — «Объекты» — «Удалить все значки». Это не самый удачный путь, удаление объектов можно проводить функцией ObjectsDeleteAll(номер_окна, тип_объекта). Она возвращает количество объектов, которое было удалено. Выведем это значение на график с помощью функции Comment(строка_вывода). Эта функция удобна для использования в индикаторах и советниках для отражения необходимой текущей информации. Теперь у нас есть скрипт, в котором учтено все для правильной работы.
Для того, чтобы приблизиться к написанию первого индикатора, мы немного доработаем скрипт CrossMASignals. Добавим в наш скрипт возможность записи в файл сигналов покупок и продаж, для этого разместим в начале скрипта блок открытия файла, в цикле добавим операцию записей сигналов, в конце пропишем закрытие файла. Назовем этот скрипт CrossMASignals-2. Можно заметить, что бары на графике делятся на два вида – бары, на которых стоят стреки вверх или вниз, и бары, на которых стрелок нет. Будем записывать в файл время открытия бара, номер бара, цена, по которой рисуется стрелка вверх, и цена , по которой рисуется стрелка вниз. То есть, у нас будет csv-файл, содержащий 4 столбца: Time[Индекс_Бара], Индекс_Бара, Цена_Синей_Стрелки, Цена_Красной_Стрелки. Бары, на которых стрелок нет, будут содержать в двух последних столбцах нулевые значения. Я добавил несколько строк в начале скрипта:
int start() { double maLongCur, maLongPrev, maShortCur, maShortPrev; string arrowName; // здесь будут назначаться уникальное имя объекта-стрелки int arrowCounter=0; int deletedArrows; string FileName; int FileHandle; // сформируем имя файла FileName="CrossMA.csv"; //откроем файл с именем FileName (создадим указатель/handle на него) FileHandle=FileOpen(FileName,FILE_WRITE | FILE_CSV,";"); if (FileHandle<1) { Print("Не удалось открыть файл, ошибка ",GetLastError()); return; } // запишем названия столбцов (создание шапки) FileWrite(FileHandle,"Дата","Номер бара","Стрелка вверх","Стрелка вниз"); // удалим все стрелки, если они есть deletedArrows=ObjectsDeleteAll(0,OBJ_ARROW); Comment("Удалено ",deletedArrows," обектов OBJ_ARROW");
// объявим служебные переменные, в которых будем хранить ценовые координаты стрелок double value1,value2; //---- for(int i=Bars-1;i>=0;i--) { // обнулим службные переменные value1=0.0; value2=0.0; maLongCur=iMA(NULL,0,21,0,MODE_SMA,PRICE_CLOSE,i+1);// медленная средняя на предыдущем баре maLongPrev=iMA(NULL,0,21,0,MODE_SMA,PRICE_CLOSE,i+2);// медленная средняя на предпредыдущем баре maShortCur=iMA(NULL,0,13,0,MODE_SMA,PRICE_CLOSE,i+1);// быстрая средняя на предпредыдущем баре maShortPrev=iMA(NULL,0,13,0,MODE_SMA,PRICE_CLOSE,i+2);// быстрая средняя на предыдущем баре
//Если есть сигнал к покупке на предыдущем баре - поставим синюю стрелку вверх // по цене открытия бара if((maShortCur>maLongCur)&&(maShortPrev<maLongPrev)) { // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 241 , стрелка вверх ObjectSet(arrowName,OBJPROP_ARROWCODE,241); // зададим цвет стрелке - голубой ObjectSet(arrowName,OBJPROP_COLOR,Blue); // увеличить счетчик стрелок arrowCounter++; value1=Open[i];//запомним цену голубой стрелки } //Если есть сигнал к продаже на предыдущем баре - поставим красную стрелку вниз // по цене открытия бара if((maShortCur<maLongCur)&&(maShortPrev>maLongPrev)) { // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 242 , стрелка вниз ObjectSet(arrowName,OBJPROP_ARROWCODE,242); // зададим цвет стрелке - красный ObjectSet(arrowName,OBJPROP_COLOR,Red); // увеличить счетчик стрелок arrowCounter++; value2=Open[i];//запомним цену красной стрелки } //запишем данные для текущего бара в файл FileWrite(FileHandle,TimeToStr(Time[i]),i,value1,value2); } //закроем файл (освободим указатель/handle, чтобы файл можно было //открыть для редактирования другими программами) if(FileHandle>0) FileClose(FileHandle);
return(0); }
Если внимательно рассмотреть получившийся код, то можно заметить, что функционально он состоит из трех частей:
Блок подготовки, в котором удаляются старые значки(стрелки) и открывается файл для записи.
Блок нанесения стрелок на график и записи значений стрелок в файл.
Завершающий блок, в нем мы закрываем файл.
В языке MQL-4 существуют три предопределенные процедуры, которые выполняется в определенном порядке – при запуске кода (скрипт, советник или индикатор) сначала выполняется функция init() (если она определена), в которой удобно провести подготовительные работы – инициализацию. В ней задаются значения необходимых переменных, заполняются глобальные массивы, открываются файлы и так далее. В блоке init() нельзя делать больших вычислительных работ, которые тормозят работу кода. Далее выполняется функция start(), в ней производится вся основная работа, пока код работает в памяти терминала, это основная рабочая функция. В ней производятся торговые операции (советники и скрипты), рассчитываются значения индикаторов, производится обработка любых доступных значений. Последней выполняется функция deinit() (если она определена), в которой производится завершение кода. Именно эта функция выполняется, когда мы снимаем с графика индикатор или советник. Эта функция также не может быть перегружена вычислительными действиями, на ее выполнение терминал отводит не более 2,5 секунд, после чего она будет принудительно завершена.Более подробную информацию можно получить во встроенной справке по теме «Выполнение программ» . Изменим наш скрипт в соответствие с этими стандартами и назовем CrossMASignals-3. Я добавил в начало каждой функции оператор Print(), чтобы можно было видеть последовательность выполнения этих функций. Кроме того, переменную
int FileHandle; // указатель на на файл - глобальная переменная
я вынес в самое начало скрипта, за пределы функций start, init и deinit. Такие переменные называются глобальными, обратиться к таким переменным можно из любого места программы, говорят, что эти переменные видимы на глобальном уровне. Мы к ней обращаемся в init() при создании указателя на файл, в блоке start() для записей в файл и в блоке deinit() при закрытии файла. В то же время переменную
string FileName; // имя файла - локальная переменная
оставил в блоке init(), так как больше она нигде не используется. Эта переменная уничтожается в памяти терминала сразу после выполнения блока init() и попытка обратиться к ней в других функциях скрипта (start или deinit) приведет к выдаче сообщения об ошибке. Такие переменные, которые живут только в пределах той функции, в которой они объявлены, называют локальными переменными. Мы даже можем объявить такую же переменную в остальных функциях, и каждая из них не будет знать о существовании другого близнеца, но делать так не рекомендуется, так как будет очень легко самому запутаться в коде.
int FileHandle; // указатель на на файл - глобальная переменная int init() { string FileName; // имя файла - локальная переменная int deletedArrows; // сформируем имя файла Print("Выполняется init()"); FileName="CrossMA.csv"; //откроем файл с именем FileName (создадим указатель/handle на него) FileHandle=FileOpen(FileName,FILE_WRITE | FILE_CSV,";"); if (FileHandle<1) { Print("Не удалось открыть файл, ошибка ",GetLastError()); return; } // запишем названия столбцов (создание шапки) FileWrite(FileHandle,"Дата","Номер бара","Стрелка вверх","Стрелка вниз"); // удалим все стрелки, если они есть deletedArrows=ObjectsDeleteAll(0,OBJ_ARROW); Comment("Удалено ",deletedArrows," обектов OBJ_ARROW");
} //---- int deinit() { Print("Выполняется deinit()"); //закроем файл (освободим указатель/handle, чтобы файл можно было //открыть для редактирования другими программами) if(FileHandle>0) FileClose(FileHandle); return(0); } //---- int start() { double maLongCur, maLongPrev, maShortCur, maShortPrev; string arrowName; // здесь будут назначаться уникальное имя объекта-стрелки int arrowCounter=0;// счетчик стрелок // объявим служебные переменные, в которых будем хранить ценовые координаты стрелок double value1,value2; Print("Выполняется start()"); for(int i=Bars-1;i>=0;i--) { // обнулим службные переменные value1=0.0; value2=0.0; maLongCur=iMA(NULL,0,21,0,MODE_SMA,PRICE_CLOSE,i+1);// медленная средняя на предыдущем баре maLongPrev=iMA(NULL,0,21,0,MODE_SMA,PRICE_CLOSE,i+2);// медленная средняя на предпредыдущем баре maShortCur=iMA(NULL,0,13,0,MODE_SMA,PRICE_CLOSE,i+1);// быстрая средняя на предпредыдущем баре maShortPrev=iMA(NULL,0,13,0,MODE_SMA,PRICE_CLOSE,i+2);// быстрая средняя на предыдущем баре
//Если есть сигнал к покупке на предыдущем баре - поставим синюю стрелку вверх // по цене открытия бара if((maShortCur>maLongCur)&&(maShortPrev<maLongPrev)) { // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 241 , стрелка вверх ObjectSet(arrowName,OBJPROP_ARROWCODE,241); // зададим цвет стрелке - голубой ObjectSet(arrowName,OBJPROP_COLOR,Blue); // увеличить счетчик стрелок arrowCounter++; value1=Open[i];//запомним цену голубой стрелки } //Если есть сигнал к продаже на предыдущем баре - поставим красную стрелку вниз // по цене открытия бара if((maShortCur<maLongCur)&&(maShortPrev>maLongPrev)) { // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 242 , стрелка вниз ObjectSet(arrowName,OBJPROP_ARROWCODE,242); // зададим цвет стрелке - красный ObjectSet(arrowName,OBJPROP_COLOR,Red); // увеличить счетчик стрелок arrowCounter++; value2=Open[i];//запомним цену красной стрелки } //запишем данные для текущего бара в файл FileWrite(FileHandle,TimeToStr(Time[i]),i,value1,value2); } return(0); } Теперь у нас все готово, но код скрипта на самом деле не очень удобен. У нас жестко задано, что период медленной средней равен 21, а период быстрой средней равен 13. Нет необходимой гибкости, в следующий раз, когда мы захотим проверить другую пару периодов, нам придется менять эти значения в коде и заново компилировать, что не очень удобно. Для таких случаев существуют внешние переменные – параметры индикаторов, советников и скриптов. Введем новые переменные LongPeriod и ShortPeriod, зададим им начальные значения, а сами переменные объявим с ключевым словом extern (внешний).
Вычисление значений средних теперь будет выглядеть так:
Мы уже имеем 4-ый вариант такого простого скрипта.
extern int LongPeriod=21; // период медленной средней extern int ShortPeriod=13; // период быстрой средней
int FileHandle; // указатель на на файл - глобальная переменная int init() { string FileName; // имя файла - локальная переменная int deletedArrows; // сформируем имя файла Print("Выполняется init()"); FileName="CrossMA.csv"; //откроем файл с именем FileName (создадим указатель/handle на него) FileHandle=FileOpen(FileName,FILE_WRITE | FILE_CSV,";"); if (FileHandle<1) { Print("Не удалось открыть файл, ошибка ",GetLastError()); return; } // запишем названия столбцов (создание шапки) FileWrite(FileHandle,"Дата","Номер бара","Стрелка вверх","Стрелка вниз"); // удалим все стрелки, если они есть deletedArrows=ObjectsDeleteAll(0,OBJ_ARROW); Comment("Удалено ",deletedArrows," обектов OBJ_ARROW");
} //---- int deinit() { Print("Выполняется deinit()"); //закроем файл (освободим указатель/handle, чтобы файл можно было //открыть для редактирования другими программами) if(FileHandle>0) FileClose(FileHandle); return(0); } //---- int start() { double maLongCur, maLongPrev, maShortCur, maShortPrev; string arrowName; // здесь будут назначаться уникальное имя объекта-стрелки int arrowCounter=0;// счетчик стрелок // объявим служебные переменные, в которых будем хранить ценовые координаты стрелок double value1,value2; Print("Выполняется start()"); for(int i=Bars-1;i>=0;i--) { // обнулим службные переменные value1=0.0; value2=0.0; maLongCur=iMA(NULL,0,LongPeriod,0,MODE_SMA,PRICE_CLOSE,i+1);// медленная средняя на предыдущем баре maLongPrev=iMA(NULL,0,LongPeriod,0,MODE_SMA,PRICE_CLOSE,i+2);// медленная средняя на предпредыдущем баре maShortCur=iMA(NULL,0,ShortPeriod,0,MODE_SMA,PRICE_CLOSE,i+1);// быстрая средняя на предпредыдущем баре maShortPrev=iMA(NULL,0,ShortPeriod,0,MODE_SMA,PRICE_CLOSE,i+2);// быстрая средняя на предыдущем баре
//Если есть сигнал к покупке на предыдущем баре - поставим синюю стрелку вверх // по цене открытия бара if((maShortCur>maLongCur)&&(maShortPrev<maLongPrev)) { // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 241 , стрелка вверх ObjectSet(arrowName,OBJPROP_ARROWCODE,241); // зададим цвет стрелке - голубой ObjectSet(arrowName,OBJPROP_COLOR,Blue); // увеличить счетчик стрелок arrowCounter++; value1=Open[i];//запомним цену голубой стрелки } //Если есть сигнал к продаже на предыдущем баре - поставим красную стрелку вниз // по цене открытия бара if((maShortCur<maLongCur)&&(maShortPrev>maLongPrev)) { // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 242 , стрелка вниз ObjectSet(arrowName,OBJPROP_ARROWCODE,242); // зададим цвет стрелке - красный ObjectSet(arrowName,OBJPROP_COLOR,Red); // увеличить счетчик стрелок arrowCounter++; value2=Open[i];//запомним цену красной стрелки } //запишем данные для текущего бара в файл FileWrite(FileHandle,TimeToStr(Time[i]),i,value1,value2); } return(0); } Но тут появляется новый ньюанс. При запуске советника или индикатора на графике появляется диалоговое окно, в котором можно задать входные параметры, но при запуске скрипта (как в нашем случае) такого нет. В этом случае, если требуется , чтобы скрипт получил входные параметры (предложил их ввести пользователю) используется директива
#property show_inputs
Добавим эту строчку и получим пятый окончательный вариант. При использовании его на графике увидим:
Если просто закрыть диалоговое окно, то увидим по закладке «Эксперты», что ни одна из функций не вызывалась, скрипт был удален сразу без исполнения. Откроем файл CrossMA.csv , в нем 4 столбца. Два последних столбца нам понадобятся для написания индикатора.
Теперь мы вплотную подошли к пониманию пользовательских индикаторов в МТ4. Сделаем настоящий, а не графический индикатор по сигналам пересечения двух скользящих средних (МА). Файл CrossMA.csv содержит столбцы «Стрелка вверх» и «Стрелка вниз». Каждому бару однозначно в текущий момент времени соответствует время открытия бара (Time[]), номер бара (который будет увеличиваться с каждым новым завершенным баром) и значения из двух столбцов/массивов, указывающих на наличие красной или синей стрелки, или отсутствие обеих (нулевое значение). Создадим пользовательский индикатор с помощью «Мастера создания советника».
Укажем имя индикатора (CrossMA), автора, линк. Рядом с таблицей параметров жмем кнопку «Добавить», появляется новый параметр ExtParam1 (имя параметра по умолчанию ), даем новому параметру более осмысленное название LongPeriod, как в нашем предыдущем скрипте, тип int оставляем неизменным, начальное значение задаем равным 21.
Добавляем еще один параметр ShortPeriod, работа с внешними параметрами нашего индикатора на этом закончена. Идем «Далее». Чек-бокс «Индикатор в отдельном окне» не трогаем, наши стрелки будут располагаться на графике цен (Чек-боксы «Минимум» и «Максимум» поэтому нам недоступны). Опять жмем «Добавить», в списке индексов на строчке 1 появляется индекс типа «Line» и красного цвета («Red»), меняем тип на «Arrow» цвет на голубой «Blue». Тип символа 217(стрелка вверх) поменяем на 241 для полного сходства со скриптом.
Добавим второй индекс, также изменим все его данные (тип, цвет, символ).
#property copyright "MetaQuotes" #property link "http://forum.alpari-idc.ru/viewtopic.php?t=48186" #property indicator_chart_window #property indicator_buffers 2 #property indicator_color1 Blue #property indicator_color2 Red //---- input parameters extern int LongPeriod=21; extern int ShortPeriod=13; //---- buffers double ExtMapBuffer1[]; double ExtMapBuffer2[]; //---- int init() { SetIndexStyle(0,DRAW_ARROW); SetIndexArrow(0,241); SetIndexBuffer(0,ExtMapBuffer1); SetIndexEmptyValue(0,0.0); SetIndexStyle(1,DRAW_ARROW); SetIndexArrow(1,242); SetIndexBuffer(1,ExtMapBuffer2); SetIndexEmptyValue(1,0.0); //---- return(0); } //---- int deinit() { //---- //---- return(0); } //---- int start() { int counted_bars=IndicatorCounted(); return(0); }
Мастер по созданию советников сделал черновую работу, мы видим, что индикатор будет выводиться на графике цен, а не в отдельном окне, индикатор содержит два служебных буфера, указаны их цвета, объявлены массивы
double ExtMapBuffer1[]; double ExtMapBuffer2[];
в которых, видимо, и будут храниться значения индикатора. Также созданы два входных параметра.
По сравнению с тем, как мы создавали скрипт, при создании индикатора была также создана функция init(), в которой производится окончательная настройка нашего индикатора. Индикаторы в МТ4 обладают очень большой гибкостью, каждый индикатор может «нести» в себе до 8 служебных(индикаторных) буферов. При этом достаточно только правильно объявить эти буфера, а остальную заботу о размере этих буферов возьмет на себя терминал. Эти буфера также называются индексами, пользовательский индикатор может выдавать значения для нулевого индекса, первого индекса , второго и так далее до седьмого индекса. Количество индексов мы задаем директивой
#property indicator_buffers N
но цвета при этом задаются не с нуля, а с единицы
#property indicator_color1 Blue #property indicator_color2 Red
С помощью SetIndexStyle(номер_индекса, стиль) мы задаем каждому индексу необходимый стиль (каждый индикатор может иметь несколько индексов с разными стилями рисования), указав на стиль Arrow мы далее уточнили код символа (241 и 242) для каждого индекса, и только потом было указано, что индекс 0 – это массив ExtMapBuffer1, индекс 1 – это данные из массива ExtMapBuffer2. Без такого связывания индекса и массива функцией SetIndexBuffer(номер_индекса, имя_массива_буфера) индикатор работать не будет, это одна из ошибок при написании индикаторов. Вторая возможная ошибка (которую очень сложно бывает обнаружить) – объявление буферов типом, отличным от double, например int .
Последней строчкой идет функция SetIndexEmptyValue(номер_индекса, Пустое_Значение). В языках программирования обычно нет понятия "отсутствия значения", если у нас есть понятие "Свет", то понятие "Тьма" в программировании будет не отсутствие Света, а очень мало Света. Поэтому важно указать, что какое-то определенное значение будет означать отсутствие значения (в нашем случае – отсутствие стрелки), обычно таким значением бывает 0.0 (ноль с точностью до одного знака после запятой), хотя можно использовать и другие значения. Осталось только написать алгоритм, который и заполнит индикаторные буфера (индексы) значениями, на которых будут располагаться стрелки. Для этого обратимся к последнему скрипту CrossMASignals-5, скопируем все из блока start(), уберем операции с нанесением стрелок на график и записью в файл. Я также добавил функции Print() в блоки init() и deinit(). Мы практически сделали индикатор, не написав еще кода, а только скопировав некоторые куски из скрипта и закомментировав ненужные места. Если поставить /* в начале фрагмента, а */ в конце фрагмента, то весь фрагмент станет комментарием, которые, как мы знаем, не компилируются. Это бывает удобно при поисках ошибок и необходимости отключать при компиляции большие куски кода.
#property copyright "MetaQuotes" #property link "http://forum.alpari-idc.ru/viewtopic.php?t=48186" #property indicator_chart_window #property indicator_buffers 2 #property indicator_color1 Blue #property indicator_color2 Red //---- input parameters extern int LongPeriod=21; extern int ShortPeriod=13; //---- buffers double ExtMapBuffer1[]; double ExtMapBuffer2[]; //---- int init() { //---- indicators Print("Выполняется init()"); SetIndexStyle(0,DRAW_ARROW); SetIndexArrow(0,241); SetIndexBuffer(0,ExtMapBuffer1); SetIndexEmptyValue(0,0.0); SetIndexStyle(1,DRAW_ARROW); SetIndexArrow(1,242); SetIndexBuffer(1,ExtMapBuffer2); SetIndexEmptyValue(1,0.0); return(0); } //---- int deinit() { Print("Выполняется deinit()"); return(0); } //---- int start() { int counted_bars=IndicatorCounted(); //---- double maLongCur, maLongPrev, maShortCur, maShortPrev; /* string arrowName; // здесь будут назначаться уникальное имя объекта-стрелки int arrowCounter=0;// счетчик стрелок */ // объявим служебные переменные, в которых будем хранить ценовые координаты стрелок double value1,value2; Print("Выполняется start()"); for(int i=Bars-1;i>=0;i--) { // обнулим службные переменные value1=0.0; value2=0.0; maLongCur=iMA(NULL,0,LongPeriod,0,MODE_SMA,PRICE_CLOSE,i+1);// медленная средняя на предыдущем баре maLongPrev=iMA(NULL,0,LongPeriod,0,MODE_SMA,PRICE_CLOSE,i+2);// медленная средняя на предпредыдущем баре maShortCur=iMA(NULL,0,ShortPeriod,0,MODE_SMA,PRICE_CLOSE,i+1);// быстрая средняя на предпредыдущем баре maShortPrev=iMA(NULL,0,ShortPeriod,0,MODE_SMA,PRICE_CLOSE,i+2);// быстрая средняя на предыдущем баре
//Если есть сигнал к покупке на предыдущем баре - поставим синюю стрелку вверх // по цене открытия бара if((maShortCur>maLongCur)&&(maShortPrev<maLongPrev)) { /* // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 241 , стрелка вверх ObjectSet(arrowName,OBJPROP_ARROWCODE,241); // зададим цвет стрелке - голубой ObjectSet(arrowName,OBJPROP_COLOR,Blue); // увеличить счетчик стрелок arrowCounter++; */ value1=Open[i];//запомним цену голубой стрелки } //Если есть сигнал к продаже на предыдущем баре - поставим красную стрелку вниз // по цене открытия бара if((maShortCur<maLongCur)&&(maShortPrev>maLongPrev)) { /* // Поставим стрелку // имя объекта - arrowName // тип объекта - OBJ_ARROW // координата по горизонтали - Time[i] время открытия бара // координата по вертикали - Open[i] цена открытия бара arrowName="arrow"+arrowCounter; ObjectCreate(arrowName,OBJ_ARROW,0,Time[i],Open[i]); // зададим тип стрелки - 242 , стрелка вниз ObjectSet(arrowName,OBJPROP_ARROWCODE,242); // зададим цвет стрелке - красный ObjectSet(arrowName,OBJPROP_COLOR,Red); // увеличить счетчик стрелок arrowCounter++; */ value2=Open[i];//запомним цену красной стрелки } /* //запишем данные для текущего бара в файл FileWrite(FileHandle,TimeToStr(Time[i]),i,value1,value2); */ } return(0); }
Код отлично компилируется, но рисовать наш индикатор ничего не будет. Для этого нам необходимо добавить только две строчки:
ExtMapBuffer1[i]=value1; ExtMapBuffer2[i]=value2;
Насколько мы помним, CrossMA.csv(после выполнения скрипта CrossMASignals-5) содержит два столбца : «Стрелка вверх» и «Стрелка вниз». Теперь эти столбцы у нас находятся в буферах ExtMapBuffer1[] и ExtMapBuffer2[] . Компилируем индикатор и запускаем его опять на графике EURUSD D1. У нас снова имеется на графике два набора стрелок (красные и синие), но только стрелки меньше размером и они уже не являются графическими объектами, доступными для редактирования, стрелки стали неотъемлемой частью индикатора и будут перерисовываться при смене тайм-фрейма. Откроем панель «Окно данных» и наведем перекрестие на последнюю синюю стрелку вверх (чтобы получить режим перекрестия, достаточно нажать среднюю кнопку мыши или колесико). Напротив CrossMA находится значение 1.1886 – это и есть значение буфера ExtMapBuffer1 на данном баре, value2 – это значение буфера ExtMapBuffer2 на данном баре, значение для него отсутствует, так как 0.0 является пустым значением ( помните
Как видите, ничего сложного в создании индикатора нет. Сам индикатор можно скачать здесь
Индикатор, который мы создали, несмотря на свой малый размер, обладает большим недостатком. Этот индикатор потребляет слишком много ресурсов процессора. Если посмотреть закладку «Эксперты» (в который видны логи, создаваемые программами MQL-4), то увидим множество выводов функции Print()(речь идет об индикаторе CrossMA-2.mq4):
Так как, функция start() вызывается на каждом тике, цикл for() выполняется при каждом изменении цены. Если вы обратили внимание, «Мастер создания советников» создал переменную counted_bars (посчитанные бары), рассмотрим ее поведение при работе индикатора. Я добавил новую переменную limit, а также сделал вывод на печать .
Логика работы индикатора не изменилась, по-прежнему цикл начинает считать от i=Bars-1 (limit=Bars-1). Но наблюдение за выводом Print() в блоке start() позволяет улучшить работу индикатора.
Видим, что при запуске индикатора, при первом вызове start() counted_bars равен нулю, Bars равен 4413, а limit равен 4412 (на единицу меньше, чем Bars). При втором вызове counted_bars уже равен 4412 и в дальнейшем не меняется на текущем баре. Получается, что значения индикатора рассчитываются полностью в первый раз, а потом на каждом тике происходит ненужный повторный расчет, вместо того, чтобы рассчитывать значения индикатора только на нулевом баре, который еще не завершился. Если мы сможем уклониться от ненужного расчета, то существенно разгрузим процессор компьютера. Представьте, что у нас открыто 10 графиков, и на каждом висят по три подобных неоптимизированных индикатора. При первом запуске counted_bars=0, проверим это условие, и если оно выполняется, то ограничитель limit оставим как есть.
Если индикатор уже рассчитывался, то будем считать только на нулевом баре. Ноль получим как Bars-counted_bars-1 (4413 – 4412 -1).
Новый модернизированный индикатор подтверждает наши предположения:
Теперь, в первый раз индикатор рассчитывается на 4413 барах, а последующие вычисления производятся только для одного (нулевого) бара. Что будет, когда текущий бар завершится и откроется новый бар? Ответ на этот вопрос легко получить самому, если пронаблюдать вывод индикатора на минутном тайм-фрейме. Полезно для любого нового индикатора, который вы написали, вставить именно эту строчку: Print("Bars=",Bars," limit=",limit," counted_bars=",counted_bars); Это позволит сразу увидеть первую возможную ошибку – неоптимизированный алгоритм расчета индикатора. Вторую распространенную ошибку (пережатость алгоритма расчета или переоптимизация) мы рассмотрим позднее.
#property copyright "MetaQuotes" #property link "http://forum.alpari-idc.ru/viewtopic.php?t=48186"
#property indicator_chart_window #property indicator_buffers 2 #property indicator_color1 Blue #property indicator_color2 Red //---- input parameters extern int LongPeriod=21; extern int ShortPeriod=13; //---- buffers double ExtMapBuffer1[]; double ExtMapBuffer2[]; //---- int init() { Print("Выполняется init()"); SetIndexStyle(0,DRAW_ARROW); SetIndexArrow(0,241); SetIndexBuffer(0,ExtMapBuffer1); SetIndexEmptyValue(0,0.0); SetIndexStyle(1,DRAW_ARROW); SetIndexArrow(1,242); SetIndexBuffer(1,ExtMapBuffer2); SetIndexEmptyValue(1,0.0); return(0); } //---- int deinit() { Print("Выполняется deinit()"); return(0); } //---- int start() { int counted_bars=IndicatorCounted(); int limit; // ограничитель расчетов double maLongCur, maLongPrev, maShortCur, maShortPrev; // объявим служебные переменные, в которых будем хранить ценовые координаты стрелок double value1,value2;
if (counted_bars==0) limit=Bars-1; // посчитанных баров еще нет, будет считать с самого начала
if (counted_bars>0) limit=Bars-counted_bars-1; // вычтем из числа доступных баров количество //посчитанных баров и уменьшим на единицу Print("Bars=",Bars," limit=",limit," counted_bars=",counted_bars);
for(int i=limit;i>=0;i--) { // обнулим службные переменные value1=0.0; value2=0.0; maLongCur=iMA(NULL,0,LongPeriod,0,MODE_SMA,PRICE_CLOSE,i+1);// медленная средняя на предыдущем баре maLongPrev=iMA(NULL,0,LongPeriod,0,MODE_SMA,PRICE_CLOSE,i+2);// медленная средняя на предпредыдущем баре maShortCur=iMA(NULL,0,ShortPeriod,0,MODE_SMA,PRICE_CLOSE,i+1);// быстрая средняя на предпредыдущем баре maShortPrev=iMA(NULL,0,ShortPeriod,0,MODE_SMA,PRICE_CLOSE,i+2);// быстрая средняя на предыдущем баре
//Если есть сигнал к покупке на предыдущем баре - поставим синюю стрелку вверх // по цене открытия бара if((maShortCur>maLongCur)&&(maShortPrev<maLongPrev)) { value1=Open[i];//запомним цену голубой стрелки } //Если есть сигнал к продаже на предыдущем баре - поставим красную стрелку вниз // по цене открытия бара if((maShortCur<maLongCur)&&(maShortPrev>maLongPrev)) { value2=Open[i];//запомним цену красной стрелки } ExtMapBuffer1[i]=value1; ExtMapBuffer2[i]=value2; } //---- return(0); }
На первый взгляд может показаться, что проблема оптимального расчета пользовательского индикатора не настолько серьезна, тем более, что с каждым годом выходят все более мощные процессоры, и новые компьютеры становятся настоящими числомолотилками. Тем не менее, навыки правильного написания индикатора пригодятся в любом виде деятельности, так как они приучают мыслить логически и экономно, а для трейдера эта черта крайне необходима. Чтобы показать важность правильного подхода, мы немного забежим вперед и прогоним простейшего эксперта, который не открывает сделок и вообще не совершает никаких торговых операций, а только содержит вызов нашего индикатора на каждом новом баре один раз. Я создал два советника, один содержит вызовы «правильного» - оптимизированного индикатора, а второй содержит вызовы «неправильного» индикатора, наш самый первый рабочий вариант индикатора. Для вызова пользовательских индикаторов используется функция iCustom(Инструмент,Тайм_фрейм,Имя_индикатора, параметры_пользовательского_индикатора, номер_индекса, номер_бара). Фактически, проверяются первые три параметра и два последних параметра, а остальные параметры, которые расположены между ними, считаются параметрами индикатора и передаются непосредственно пользовательскому индикатору. Правильность передаваемых параметров компилятором не проверяется, так как связывание(вызов) пользовательского индикатора происходит динамически, на лету. Вызов нашего индикатора будет выглядеть так:
Вызов пользовательского индикатора без параметров будет равнозначен вызову индикатора с параметрами по умолчанию (в нашем случае 21 и 13), и поэтому такая запись будет аналогична предыдущей:
Параметры пользовательского индикатора также отражены в свойствах индикатора:
Я сделал два советника, один содержит вызов оптимизированного индикатора (CrossMA-3) с именем TestCrossMA, а второй содержит вызов неоптимизированного индикатора(CrossMA-2) , назван TestCrossMAwrong. Скачать их можно здесь . Сохраните их в папке /experts, скомпилируйте, затем в терминале откройте «Тестер стратегий» (Ctrl+R) , установите необходимые параметры и нажмите кнопку «Старт». На моем компьютере (Celeron-2000) прогон оптимизированного индикатора занял чуть больше 1 секунды, а неоптимизированного более 30 секунд
Этот пример наглядно показывает необходимость оптимального расчета пользовательского индикатора, если запустить эти два советника для тестирования на меньших тайм-фреймах (H4, H1 и так далее), то разрыв будет еще большим.
Мы узнали, как создать индикатор, который рисует символы/стрелки на пересечениях двух средних с разным периодом. Теперь мы напишем индикатор, который строит простую скользящую среднюю. Скользящие средние одни из самых важных в техническом анализе, на их основе строится подавляющая часть других индикаторов, как стандартных, так и разрабатываемых самими трейдерами. Скользящая средняя характеризуется периодом расчета, типом цен, по которым рассчитывается и смещением. Если мы хотим рассчитать простую среднюю с периодом 3 по ценам закрытия, то мы должны взять сумму: ( цена закрытия сегодня + цена закрытия вчера + цена закрытия позавчера)/3. МА(3,Close)=(Close[0]+Close[1]+Close[2])/3. Обратимся опять к «Мастеру создания Советника». Создадим параметр PeriodMA и ShiftMA (смещение).
Следующим шагом добавим индекс , тип и цвет оставим как есть, жмем «Готово».
Получили стандартный шаблон:
Добавим в блок start() часть кода из индикатора CrossMA-3, чтобы не писать заново цикл и служебные переменные.
Мы уже хорошо представляем как происходит работа в цикле for(), теперь мы можем заменить цикл for() на цикл while(). Выносим объявление переменной i (int i=limit;) за пределы for() перед циклом, операцию декремента (i--;) вносим в тело цикла в самый конец, цикл for переименовываем на while:
Как видим, цикл while() полностью заменил цикл for(). Также производится проверка выражения в скобках на истинность, и если оно соблюдается, то выполняется тело цикла. При этом важно не забыть вставить в цикл оператор уменьшения счетчика i, иначе мы получим бесконечный цикл. Компилятор такую ошибку обнаружить не сможет (так как она не синтаксическая, а логическая), и при использовании такого неправильного индикатора (с бесконечным циклом) терминал просто зависнет и перестанет реагировать на действия пользователя. Единственный способ выхода из такой ситуации – закрыть принудительно из диспетчера задач терминал, открыть MetaEditor, исправить индикатор и можно снова запускать MetaTrader4. Но в скриптах использовать бесконечный цикл можно.
Применение цикла while() или for() обычно является делом вкуса, хотя бывают ситуации, когда while() предпочтительней , например, если необходимо двигаясь от текущего бара в глубь истории (в сторону увеличения индекса бара) найти некий бар, удовлетворяющий какому-то условию. При этом необходимо не забыть, что искомый бар может и не быть найденным, а индекс бара будет расти до бесконечности. И чтобы избежать зацикливания, необходимо добавить проверку индекса на превышение числа Bars. Пример такого цикла:
Цикл у нас есть, теперь осталось написать алгоритм вычисления суммы на количестве баров PeriodMA, то есть что-то подобное:
Чему будет равно n, если известно PeriodMA (период нашей средней)? Есть люди, которые легко вычислят нужную формулу в уме, я много раз ошибался сам, когда проделывал это без помощи бумаги и ручки, поэтому более надежно будет пойти методом индукции. Времени займет минуту, а правильность результата гарантирует. Возьмем частный случай, n=5, то есть как будто мы ищем среднее за 5 баров. Тогда наша формула выглядит так:
Такое механическое суммирование мы легко поместим опять-таки в цикл for().
В данном случае мы использовали оба типа циклов, получилось удобное визуальное разделение алгоритма прохода по барам графика и алгоритма суммирования цен. Теперь опять вернемся к началу блока start(). Как видно из рисунка, устанавливать ограничитель расчета limit при первом вызове индикатора в значение Bars-1 нет никакого обоснования, баров в глубине истории нет и усреднять еще нечего. Если мы для примера хотим рассчитать среднюю с периодом 5, то вычислять эту среднюю мы можем начать только с бара Bars-5 (см. статью «Что такое графики»).
Поэтому, в первый раз, когда counted_bars равно нулю логично записать:
На этом создание индикатор простой скользящей средней по ценам закрытия можно считать законченным, но иногда возникает соблазн создать максимально оптимизированный индикатор, и делается попытка улучшить и ту часть блока start(), где counted_bars уже больше нуля, то есть первоначальный расчет индикатора уже проводился. Исходя из соображений, что после первого прогона индикатора Bars не изменился, расчет индикатора производился на количестве баров от Bars-PeriodMA до нуля (то есть counted_bars предполагается равным примерно Bars-PeriodMA), пишется следующее :
Так как преследуется цель считать только на нулевом баре, то можно предположить, что limit=0 и 0=Bars-counted_bars-PeriodMA. Отсюда следует, что мы исходим из того, что при втором прогоне индикатора counted_bars+PeriodMA==Bars. На самом деле это не так, это вторая ошибка оптимизации, я называю такой индикатор пережатым. Функция IndicatorCounted() не возвращает количество баров, для которых был сделан расчет значений данным конкретным индикатором, а возвращает количество баров, которые не изменились с последнего вызова индикатора (нулевой бар менялся, иначе бы не пришел этот тик, на котором мы и производим проверку). Поэтому, если Bars=1000, то при первом вызове индикатора IndicatorCounted() вернет 0 (индикатор еще не рассчитывался), а при втором вызове IndicatorCounted() вернет уже Bars-1. Вы сами может проверить это , создав пустой индикатор (в котором не производится вообще никаких расчетов) и вставив в блок start() вызов
Если мы все же не заметим этой ошибки и оставим наш индикатор таким как есть(переоптимизированным или пережатым) то произойдет следующее ( пусть у нас Barsравно 1000, а PeriodMA равно 5):
в первый раз индикатор рассчитается правильно от бара Bars-PeriodMA до нулевого бара
на следующем тике проверка counted_bars окажется больше нуля и ограничитель расчета станет равным limit=1000-999-5= 1-5=-4
в блок while() (или for(), вид цикла не имеет значения) поступает ограничитель, который меньше нуля и производится проверка while(-4>=0) , проверка оказывается неудачной , и пересчет на нулевом баре не производится.
Выявить такую ошибку можно двумя способами:
запустить индикатор на мелком тайм-фрейме и наблюдать его поведение
прогнать в тестере советник, содержащий вызов нашего индикатора.
Для данного индикатора наиболее простым вариантом проверки будет первый. Компилируем неправильный индикатор и набрасываем на EURUSD M15, ждем полчаса, чтобы появилось два новых бара и видим такую картину.
К счастью, в данном случае ошибка расчета индикатора при работе в он-лайн быстро становится явной(индикатор просто не рисуется), но в более сложных случаях значение индикатора просто плывет и на глаз увидеть это не всегда можно. Скачать индикаторы Simply MA.mq4 и Simply MA wrong.mq4 можно здесь .
Мы создали индикатор простой скользящей средней с периодом PeriodMA, теперь нам осталось задействовать параметр ShiftMA, который отвечает за смещение индикатора. Часто в теханализе требуется не только рассчитать значения индикатора, но и сместить их на какое-то количество баров вперед или назад. Это свойство называется смещением. В MQL-4 смещение индикатора достигается очень просто, достаточно любому индикаторному индексу установить признак смещения с помощью функции SetIndexShift(), для этого в блоке init() мы пишем эту простую конструкцию и получаем желаемый результат.
Набрасываем на график наш новый индикатор со смещением ноль и второй индикатор со смещением, например, три. Мы видим, что индикатор, использующий смещение, рисуется на три бара вперед, при отрицательном смещении индикатор будет рисоваться с запаздыванием. Мы можем улучшить индикатор. Когда мы смотрим в Окне Данных (Ctrl+D) значения нашего индикатора, то мы видим, что указывается имя индикатора и значение его для данного бара. Часто индикатор может содержать больше одного индекса, обычно нулевой индекс именуется по имени индикатора, а все остальные индексы именуются как Value1 , Value2 и так далее. Для того, чтобы дать более осмысленные имена индексам, служит функция SetIndexLabel(), которая задает отображаемое имя для указанного индекса. Используем в качестве имени SMA(PeriodMA), таким образом мы показали, что это простая скользящая средняя с периодом PeriodMA. Обычно эта функция также используется в блоке init().
Если мы посмотрим технический индикатор Moving Average, то увидим, что мы не использовали тип цен для расчета нашего индикатора. Обратимся к справке ME, там указано 7 стандартных ценовых констант :
Добавим новый параметр PriceType( тип цены) и изменим блок start() таким образом, чтобы наш индикатор считался не только по ценам открытия. Если мы вместо цен Close[i+k] в цикле for() будем подставлять необходимый тип цены, то больше ничего и не надо. Введем новую вспомогательную переменную double price и будем задавать ее значение в соответствии со справкой МЕ. Сделаем это для унификации пользовательского и технического индикатора, в блоке switch(PriceType) используем в качестве ключей предопределенные ценовые константы.
Кроме того, процедуру формирования метки для индекса в блоке init() сделаем также более гибкой, теперь будем отображать не только период(PeriodMA) и смещение(ShiftMA), но и тип цены, от которой построен индикатор.
На этом построение простой скользящей средней MA.mq4 будет полным.
Теперь рассмотрим еще один вариант средней. Часто бывают индикаторы, когда текущее значение индикатора зависит от предыдущего значения и текущей цены. Можно записать этот так: F[i]=F[i+1]*a+Price[i]*b, где a и b - коэффициенты, обычно меньше 1. Иногда требуют, чтобы a+b=1. Тогда F[i]=F[i+1]*(1-b)+Price[i]*b . Чем меньше коэффициент b, тем меньше реагирует индикатор на изменение цен и больше зависит от предыдущего значения индикатора. Для экспоненциальной скользящей средней(ЭСС) коэффициент b=2/(1+PeriodMA). Видим, что чем больше период ЭСС, тем более плавной будет кривая индикатора. Построим новый индикатор EMA.mq4 на базе MA.mq4. Произведем косметические изменения в блоке init()
В блоке start(), правда, появляется некоторая заминка. В самом начале, когда мы рассчитываем индикатор в первый раз, у нас нет предыдущего значения индикатора. Нам негде взять предыдущее значение и нам придется делать какое-то допущение. Первый вариант – принять соглашение, что для бара с индексом Bars-1 значение индикатора равно значению цены на этом баре (High, Open, Close или другая ценовая константа, например, {High+Low+Close}/3.0 ), а дальше рассчитывать по алгоритму. Второй вариант – для бара с индексом Bars-PeriodMA значение индикатора равно простой скользящей стредней (ПСС), а дальше опять рассчитывать по алгоритму. Первый вариант реализован в пользовательском индикаторе Moving Averages.mq4 , который идет в составе дистрибутива MetaTrader4 и находится в папке MetaTrader 4expertssamplesindicators . Поэтому мы пойдем по второму варианту, чтобы сравнить их между собой.
Блок вычисления простой средней (синего цвета) возьмем из индикатора MA.mq4. Небольшая доводка, и индикатор EMA.mq4 готов.
Видно, что в коде есть два одинаковых участка/блока, которые очень похожи друг на друга. MQL-4 позволяет объявлять свои(пользовательские) функции для однообразных вычислений. Справку по функциям можно посмотреть в «Справочнике MQL4» в разделе «Функции». Создадим функцию GetPrice(int PriceMode, int index), перенесем в нее все вычисления, тип функции зададим double.
Теперь мы заменим эти блоки одной строчкой вызова функции GetPrice() и индикатор EMA2.mq4.mq4 готов. Произведем такие же изменения с индикатором MA.mq4 и назовем его MA2.mq4. Скачать их можно здесь .
Рассмотрим на примере несложного индикатора, написанного для терминала MetaTrader3, процесс переноса кода в MQL-4. Хотя сам язык MQL-2 является существенно более простым, но некоторые особенности и его ограничения наложили свой отпечаток на стиль написания сложных индикаторов. Первое отличие- в MQL-2 индикатор мог отрисовывать не более двух индексов, поэтому в случае необходимости приходилось писать несколько родственных индикаторов (вспомним из известных индикаторы Pivot, где кроме точки вращения нужно было рисовать и линии поддержки и сопротивления). В других случаях приходилось создавать индикатор, который строился на вызовах другого пользовательского индикатора, так как была потребность в дополнительных буферах, а использовать массивы не всегда было удобно. Второе ограничение – из-за низкой скорости работы языка-интерпретатора работа индикатора (вычисления в теле индикатора или эксперта) принудительно завершалась через определенное время, приходилось использовать служебную переменную, в которой хранился индекс последнего рассчитанного бара, и при поступлении нового тика продолжать работу цикла с запомненного значения. В MQL-4 такой потребности нет, язык быстрый и для целей оптимизации была специально создана функция IndicatorsCounted(). Третье ограничение – малое число стилей отрисовки, в МТ3 была линия, гистограмма и стрелки. Сам код индикатора на MQL-2
Зеленым цветом прокомментирована интерфейсная часть, тут никаких сложностей нет. Далее в секции Inputs идут входные параметры , здесь нам необходимо только определиться каким типом их объявить в MQL-4. Переменные ppor(100),mpor(-100) используются для отрисовки второго буфера, а так индикаторные буфера в MQL-4 всегда имеют тип double, то для единообразия объявим их тем же типом. Переменная per является периодом индикатора CCI(), поэтому объявим ее типом int. Переменная t3_period(8) используется для вычисления значения типа double, хотя по смыслу должна означать некий целочисленный период, поэтому можно объявить ее и так и сяк, но я все же объявлю ее с типом double. С переменной b и так все понятно. Переменная x_prd нигде в коде не используется, ее просто выбрасываем. Все переменные (Variable), кроме shift, объявим типом double, это видно из логики вычислений. Все готово для написания индикатора. Начинаем стандартную процедуру:
Имя индикатора я поставил такое же, что было в МТ3, хотя в самом коде стоит имя RoundPrice.
Переменные можем скопировать из кода mql, поменяем Inputs на double, объявим их в интерфейсной части. Можно заметить, что эта часть кода бесполезно вычисляется каждый тик, хотя значения этих переменных не меняются:
Поэтому, мы можем эту часть вычислений поместить в ту часть индикатора, которая выполняется только один раз перед после запуска индикатора – в блок init(). Оператор if() требует минимальной правки.
Оператор SetLoopCount(0); просто игнорируем, он из прошлой жизни и в MQL-4 абсолютно не нужен никогда. Опять копируем код и приводим синтаксис к MQL-4. Открывающий оператор Begin и закрывающий оператор End заменяем фигурными скобками:
Нам осталось только перевести три выделенных блока, и работа, можно сказать, закончена. Операторы SetIndexValue() и SetIndexValue2() в MQL-2 заполняют индикаторные массивы №1 и №2. Поэтому запишем просто:
Технический индикатор iCCI() также переносится несложно, добавим только параметр инструмента и тайм-фрейма. В MQL-2 было два технических индикатора : iCCI() и iCCIEx(), разница между ними только в том, что первый строился только по ценам закрытия, а второму можно было задавать тип цен для расчетов. Можно посмотреть справку МТ3. Заодно отметим четвертое отличие МТ3 от МТ4 – подробная справочная система на русском языке. Делаем последнюю замену – индикатор готов.
Запускаем для сравнения индикатор в обоих терминалах, в МТ3
и в МТ4
Небольшая погрешность объясняется разным способом построения цены. При этом можно заметить, что индикатор в МТ3 рассчитывается заметно медленнее и на более слабых компьютерах временами даже может совсем пропадать. На моем Celeron-2000 после первого расчета окно индикатора несколько раз очищалось и приходилось запускать индикатор заново. Мы сделали, фактически, подстрочный перевод, и теперь можем немного проанализировать код индикатора, чтобы его улучшить. Улучшение будет заключаться в оптимизации механизма расчета индикатора. Первый вопрос возникает с не нужным ,вроде бы, циклом, в котором происходит обнуление обоих индикаторных массивов.
К тому же мы не использовали функцию InditarorCounted(). Закомментируем цикл и введем ограничитель расчета.
Возникает два вопроса, первый – почему при первом расчете, когда counted_bars еще равен нулю, мы устанавливаем limit равным Bars-per? Смотрим ниже по коду и видим, что в расчетах используется вызов технического индикатора iCCI() с периодом per. Набрасываем его на график и видим, что рисоваться индикатор CCI(14) начинает только с 15-го бара с конца, а до этого момента его значения не определены.
А если расчет уже проводился (counted_bars больше нуля) , то выражение Bars-counted_bars обычно равно единице (в каких случаях это не так - вы можете проверить сами). А так как для оптимизации расчетов мы будем стараться считать минимальное количество баров, то уменьшая limit на единицу, мы удовлетворяем для обоих случаев в первом случае в итоге limit=Bars-per-1, а во втором случае limit=0. На этом оптимизацию можно считать законченной, если не одно НО. Если мы повесим наш последний вариант индикатора на 15 минутки и подождем полчаса (чтобы появилось два новых бара), а затем набросим на график еще один такой же индикатор с такими же параметрами, но с другими цветами (чтобы их различать), то увидим существенную разницу.
Оказывается, наш индикатор меняет свои показания с течением времени, в таких случаях еще говорят, что индикатор перерисовывает себя. Но в данном случае это не так, просто у нас плывут значения переменных, служащих для расчета индикатора на нулевом баре. Для этого добавим вывод в лог значений этих переменных. Сохраняем наш новый индикатор под именем SmCCI-2.mq4 и начинаем его модифицировать.
Видим, что «плывут» все переменные. Теперь становится понятно для чего был необходим цикл, в котором обнулялись значения индикатора на каждом тике. Таким образом автор индикатора боролся с этим неприятным эффектом. Из-за этого очищающего цикла у меня пропадал индикатор в терминале МТ3, так как времени, отводимого индикатору в MQL-2 на каждый тик, хватало только на очистку двух буферов и не хватало на повторный полный расчет индикатора.
Нам необходимо зафиксировать (стабилизировать) переменные e1,e2… e6 и наши проблемы будут решены. Расчет каждой из этих переменных для текущего бара зависит от значения этой переменной на предыдущем баре. Это напоминает в какой-то степени алгоритм расчета экспоненциальной скользящей средней, и мы можем применить такой же способ для хранения этих переменных в индикаторных буферах. Для этого объявим e1Buffer[],e2Buffer[]…e6Buffer[].
Массивы объявлены, теперь заменим переменные соответствующими массивами. На рисунке отмечены два момента, нарушение которых ведет к неправильной работе индикатора. Это две наиболее распространенные ошибки в тех случаях, когда индикатор использует больше индикаторных буферов, чем выводится на экран. Если их допустить, то можно долго и безуспешно искать ошибку в логике индикатора.
Запускаем два варианта индикатора на графике, спустя два бара вешаем еще один, первый вариант (который «плывет») и видим, что второй вариант не меняет свои показания со временем, в отличие от первого.
Конечно, в какой-то степени рассматриваемый индикатор оказался простым, и нам повезло, что восьми индикаторных буферов оказалось достаточно для решения проблемы с данным индикатором, но на самом деле, если бы потребовалось не 8, а 9 и более индикаторных буферов (индикатор может иметь на данный момент не более восьми встроенных буферов) – то и в этом случае мы смогли бы решить эту проблему. Мы могли бы ввести дополнительные переменные, в которых хранили бы значения на предыдущем баре (что-то вроде prev_e1, prev_e2… prev_e6) или объявить просто массивы. Мне пока не приходилось встречаться с ситуациями, когда нельзя было бы написать индикатор в MQL-4. На этом перенос кода из MQL-2 в MQL-4 можно считать законченным, остальная доводка индикатора принципиально алгоритм уже не меняет. Сами индикаторы можно взять здесь .