ПОШАГОВОЕ РУКОВОДСТВО ПО НАПИСАНИЮ MQL5-СОВЕТНИКОВ ДЛЯ НАЧИНАЮЩИХ
Эта статья предназначена для начинающих, для тех, кто хочет научиться написанию простых советников на новом языке MQL5. Сначала мы определимся с тем, что требуется от нашего советника, а затем приступим к написанию того, каким образом он будет это делать.
1. Торговая стратегия
Что будет делать наш советник:
- Он будет следить за некоторыми индикаторами и при определенном условии (или условиях) помещать торговый запрос (на продажу или покупку) в зависимости от условий.
Это называется торговой стратегией. Перед тем, как написать советник, сначала нужно разработать стратегию, которую вы хотите автоматизировать в советнике. Давайте конкретизируем нашу стратегию, которую будем применять в советнике.
- Мы будем использовать индикатор Moving Average (скользящие средние) с периодом 8 (вы можете выбрать любой период, но в данной стратегии мы будем использовать период 8).
- Мы хотим, чтобы наш советник покупал, если 8-периодная скользящая средняя (далее для удобства будем называть ее MA-8) возрастает и текущая цена закрытия находится выше ее; советник должен продавать, когда MA-8 падает и цена закрытия находится ниже MA-8.
- Также мы собираемся использовать другой индикатор, называемый Average Directional Movement (ADX) с периодом 8 для определения факта наличия тренда на рынке. Это нужно для того, чтобы входить в рынок, когда он находится в состоянии тренда. Для того, чтобы это реализовать, мы будем помещать торговый запрос (на покупку или продажу) при наступлении условий, указанных выше, а также при значениях ADX, больших 22. Если ADX>22, но уменьшается или ADX<22, мы не будем помещать торговые запросы даже при наступлении условий, изложенных в пункте 2.
- Мы хотим защитить себя установкой ордеров Stop Loss в 30 пунктов, Take Proft установим на уровне 100 пунктов.
- Также мы хотим, чтобы советник проверял возможности для продажи/покупки только при формировании нового бара, при этом советник должен помещать ордер на покупку только в случае сигнала на покупку и отсутствия открытых длинных позиций. Аналогично в случае продажи — условия на продажу и отсутствие открытых коротких позиций.
Стратегия разработана, теперь время начать писать код.
2. Пишем советник
2.1 Мастер MQL5
Начнем с запуска редактора MetaQuotes Language Editor 5. Затем нажимаем Ctrl-N или на кнопку «Создать» в панели инструментов.
Рисунок 1. Создание нового документа MQL5
В окне Мастера MQL5 выбираем «Советник» и нажимаем «Далее», как показано на рис. 2:
Рисунок 2. Выбор типа создаваемой программы
В следующем окне в поле «Имя» напишите имя, которое вы хотите дать вашему советнику, я написал «My_First_EA«. Вы можете указать свое имя в поле «Автор» и адрес в виде ссылки на ваш сайт или e-mail (если есть).
Рисунок 3. Общие параметры советника
Поскольку мы хотим иметь возможность менять некоторые параметры нашего советника, для того, чтобы найти лучшие, мы добавим их при помощи кнопки «Добавить».
Рисунок 4. Входные параметры советника
В нашем советнике нам нужно иметь возможность изменять Stop Loss, Take Profit, ADX Period and Moving Average Period, так что укажем их здесь.
Дважды кликнем мышкой по колонке «Имя» в параметрах и напишем наименование параметра, аналогично в колонках «Тип» и «Начальное значение» укажем тип данных параметра и начальные значения.
После этого, результат будет примерно следующий:
Рисунок 5. Типы данных входных параметров советника
Как видно, мы выбрали тип integer (int) для всех параметров. Рассмотрим подробнее типы данных.
- char: Целый тип char занимает в памяти 1 байт (8 бит) и позволяет выразить в двоичной системе счисления 2^8 значений=256. Тип char может содержать как положительные, так и отрицательные значения. Диапазон изменения значений составляет от -128 до 127.
- uchar: Целый тип uchar также занимает в памяти 1 байт, как и тип char, но в отличие от него, uchar предназначен только для положительных значений. Минимальное значение равно нулю, максимальное значение равно 255. Первая буква u в названии типа uchar является сокращением слова unsigned (беззнаковый).
- short: Целый тип short имеет размер 2 байта(16 бит) и, соответственно, позволяет выразить множество значений равное 2 в степени 16: 2^16=65 536. Так как тип short является знаковым и содержит как положительные, так и отрицательные значения, то диапазон значений находится между -32 768 и 32 767.
- ushort: Беззнаковым типом short является тип ushort, который также имеет размер 2 байта. Минимальное значение равно 0, максимальное значение 65 535.
- int: Целый тип int имеет размер 4 байта (32 бита). Минимальное значение -2 147 483 648, максимальное значение 2 147 483 647.
- uint: Беззнаковый целый тип uint занимает в памяти 4 байта и позволяет выражать целочисленные значения от 0 до 4 294 967 295.
- long: Целый тип long имеет размер 8 байт (64 бита). Минимальное значение -9 223 372 036 854 775 808, максимальное значение 9 223 372 036 854 775 807.
- ulong: Целый тип ulong также занимает 8 байт и позволяет хранить значения от 0 до 18 446 744 073 709 551 615.
Как видно из описания различных типов данных, беззнаковые целые (uint) не предназначены для хранения отрицательных значений, любые попытки установить отрицательные значения могут привести к непредсказуемым результатам. Например, если вы хотите хранить отрицательные значения, нельзя для них использовать переменные типа uchar, uint, ushort, ulong.
Вернемся к нашему советнику. Для значений, меньших 127 или 255, для экономии памяти можно использовать значения типа char or uchar, соответственно, однако для удобства мы зададим их значения как тип int.
После того, как закончено определение необходимых входных параметров индикатора, нажмем на кнопку «Finish» и MetaQuotes Editor5 создаст шаблон кода, представленный ниже:
Для лучшего понимания, рассмотрим отдельно различные секции кода.
В верхней части кода (заголовок) определяются свойства советника. Как видно, это значения, которые были установлены в Мастере MQL5 на рис. 3.
В этой части кода также можно задать дополнительные параметры, например description (текст с кратким описанием советника), определить константы, включить дополнительные файлы или импортируемые функции.
Для выражений, начинающихся с символа «#», не нужно ставить точку с запятой в конце строки, это директивы препроцессора. Другой пример:
- #define
Директива #define используется для определения констант. Записывается в виде: - #define identifier token_string
Это означает, что компилятор заменит в коде переменные identifier численным значением, равным token_string.
Например:
#define ABC 100
#define COMPANY_NAME «MetaQuotes Software Corp.»
В данном случае COMPANY_NAME будет означать строку «MetaQuotes Software Corp.», вместо ABC будет подразумеваться число, равное 100.
Более подробнее о директивах препроцессора можно прочитать в руководстве по MQL5. Идем далее.
Вторая часть заголовка в нашем коде — это секция входных параметров.
Здесь мы задаем входные параметры, которые будут использоваться в советнике как переменные, они могут быть использованы во всех функциях нашего советника.
Переменные, определенные на этом уровне, называются глобальными переменными, поскольку они доступны из любой функции советника. Входные параметры могут быть изменены только вне кода советника при запуске. Также мы можем определить другие переменные, которые будем использовать в нашем советнике, они не будут доступны для модификации извне.
Далее идет функция инициализации советника. Это функция вызывается первой после запуска советника или смены графика и вызывается только один раз.
Этот раздел — лучшее место для проведения проверок, чтобы убедиться в правильности работы нашего советника.
Например, можно проверить, достаточно ли баров на графике для работы нашего советника и т.п.
Также это лучшее место для получения хэндлов технических индикаторов, которые будут использоваться (в нашем случае это индикаторы ADX и Moving Average).
Функция OnDeinit вызывается при удалении советника с графика.
В нашем советнике, в данной функции мы будем освобождать хэндлы индикаторов, созданных в разделе инициализации.
Данная функция производит обработку события NewTick, которое генерируется при приходе новой котировки для символа.
Большая часть кода, отвечающего за реализацию нашей торговой стратегии будет содержаться в данной функции.
Отметим, что советник не сможет производить торговые операции, если в авто-трейдинг не разрешен в клиентском терминале:
Рисунок 6. Торговля советником разрешена
Теперь, когда мы рассмотрели разделы кода нашего советника, начнем добавления кода в шаблон.
2.2. Раздел входных параметров
//--- входные параметры input int StopLoss=30; // Stop Loss input int TakeProfit=100; // Take Profit input int ADX_Period=8; // Период ADX input int MA_Period=8; // Период Moving Average input int EA_Magic=12345; // Magic Number советника input double Adx_Min=22.0; // Минимальное значение ADX input double Lot=0.1; // Количество лотов для торговли //--- глобальные переменные int adxHandle; // хэндл индикатора ADX int maHandle; // хэндл индикатора Moving Average double plsDI[],minDI[],adxVal[]; // динамические массивы для хранения численных значений +DI, -DI и ADX для каждого бара double maVal[]; // динамический массив для хранения значений индикатора Moving Average для каждого бара double p_close; // переменная для хранения значения close бара int STP, TKP; // будут использованы для значений Stop Loss и Take Profit
Как видно, мы добавили новые параметры. Перед тем, как обсудить их предназначение, посмотрим на код. При помощи двойного слэша «//» в код можно помещать комментарии. При помощи комментариев мы можем описывать предназначение наших переменных или производимые нами действия. Комментарии позволяют улучшить понимание кода. Существуют два основных способа написания комментариев:
Это однострочный комментарий.
/*
Это многострочный комментарий
*/
Это многострочный комментарий. Многострочные комментарии начинаются с пары символов «/*» и заканчиваются «*/».
При компиляции кода комментарии игнорируются компилятором.
Использование однострочных комментариев для входных параметров позволяет описать предназначение входных параметров. В этом случае вместо наименования параметров будут показаны комментарии, как показано ниже:
Рисунок 7. Входные параметры советника
Вернемся к нашему коду.
Мы решили добавить дополнительные параметры в наш советник. Параметр EA_Magic (Magic Number) будет использован для всех ордеров нашего советника. Минимальное значение ADX задано как переменная типа double. Значения типа double используются для констант, которые, наряду с целой частью, также могут содержать и дробную часть.
Например:
double mysum = 123.5678;
double b7 = 0.09876;
Количество лотов для торговли (Lot) представляет собой объем финансового инструмента, который мы хотим торговать.
Далее мы также объявили дополнительные переменные, которые будут использованы следующим образом: переменная adxHandle будет использоваться для хранения хэндла индикатора ADX, переменная maHandle для хэндла индикатора Moving Average. Динамические массивы plsDI[], minDI[], adxVal[] are будут использованы для хранения значений +DI, -DI и самого значения ADX для каждого бара графика. Численные значения индикатора Moving Average для каждого бара графика будут храниться в динамическом массиве maVal[].
Кстати, что представляют собой динамические массивы? Динамический массив — это массив, объявленный без указания размера. Другими словами, в квадратных скобках при его описании нет конкретного числа, указывающего его размер.
С другой стороны, для статических массивов их размер определяется при объявлении.
Пример:
double allbars[20]; // этот массив содержит 20 элементов — от 0 до 19
Переменная p_close будет использоваться для хранения численного значения цены Close для бара, который мы собираемся отслеживать в процессе проверки торговых сигналов на покупку и продажу.
Переменные STP и TKP нужны для установки значений Stop Loss и Take Profit ордеров нашего советника.
2.3. Секция инициализации советника
int OnInit() { //--- Получить хэндл индикатора ADX adxHandle=iADX(NULL,0,ADX_Period); //--- Получить хэндл индикатора Moving Average maHandle=iMA(_Symbol,_Period,MA_Period,0,MODE_EMA,PRICE_CLOSE); //--- Нужно проверить, не были ли возвращены значения Invalid Handle if(adxHandle<0 || maHandle<0) { Alert("Ошибка при создании индикаторов - номер ошибки: ",GetLastError(),"!!"); }
Далее мы получаем хэндлы индикаторов, используя соответствующие функции индикаторов.
Хэндл индикатора ADX получаем при помощи функции iADX. В качестве аргументов ей передается символ графика symbol (NULL также означает символ текущего графика), период/таймфрейм (0 означает таймфрейм текущего графика), период индикатора ADX, который будет использоваться для вычисления индикатора (ADX_Period мы определили в разделе входных параметров индикатора):
int iADX( string symbol, // имя символа ENUM_TIMEFRAMES period, // период int adx_period // период усреднения индикатора ADX );
Хэндл индикатора Moving Average получаем при помощи функции iMA. Аргументы этой функции следующие:
- symbol — Символьное имя инструмента, на данных которого будет вычисляться индикатор (можно использовать _symbol, symbol() или NULL для текущего символа).
- period — Значение периода может быть одним из значений перечисления ENUM_TIMEFRAMES, (можно использовать _period, period() или 0 для таймфрейма текущего графика).
- ma_period — Период усреднения для вычисления скользящего среднего (который мы определили ранее в разделе входных параметров индикатора).
- ma_shift — Сдвиг индикатора относительно ценового графика (мы используем 0).
- ma_method — Метод усреднения. Может быть любым из значений MODE_SMA, MODE_EMA, MODE_SMMA или MODE_LWMA.
- applied_price — Используемая цена. Может быть любой из ценовых констант ENUM_APPLIED_PRICE или хендлом другого индикатора.
int iMA( string symbol, // имя символа ENUM_TIMEFRAMES period, // период int ma_period, // период усреднения int ma_shift, // смещение индикатора по горизонтали ENUM_MA_METHOD ma_method, // тип сглаживания ENUM_APPLIED_PRICE applied_price // тип цены или handle );
Для получения более подробной информации, посмотрите справку по этим индикатным функциям в документации по MQL5. Это даст лучшее понимание того, как использовать этот индикатор.
Мы проверяем результат выполнения функций на наличие ошибок, в случае неудачи можем получить ошибку INVALID_HANDLE. В этом случае выводим сообщение об ошибке и ее код, используя функцию GetlastError и завершаем работу советника.
Мы решили хранить значения Stop Loss и Take Profit в определенных ранее переменных STP и TKP. Почему мы это сделали?
Это сделано потому, что значения входных параметров не могут быть модифицированы, они только для чтения.
Мы должны быть уверены в том, что наш советник будет корректно работать со всеми брокерами. Для определения точности цены котировок по текущему символу графика можно воспользоваться
Предопределенной переменной _Digits или функцией Digits(). Для 3-х и 5-ти значных котировок мы умножаем значения Stop Loss и Take Profit на 10.
2.4. Раздел деинициализации советника
Поскольку эта функция вызывается при прекращении работы советника или удалении советника с графика, здесь мы освобождаем хэндлы индикаторов, созданные в процессе инициализации. Мы создали два индикатора, ADX и Moving Average.
Для их удаления мы используем функцию IndicatorRelease(). Эта функция имеет лишь один параметр (хэндл индикатора).
bool IndicatorRelease(
int indicator_handle, // хэндл индикатора
);
Функция удаляет хэндл индикатора и освобождает расчетную часть индикатора, если ею больше никто не пользуется.
2.5 Раздел OnTick советника
Первое, что мы здесь делаем — проверяем достаточно ли баров на текущем графике. Количество баров на любом графике можно узнать при помощи функции Bars. У нее есть два входных параметра, первый — symbol, (символ текущего графика можно получить используя предопределенную переменную _Symbol или функцию Symbol()) и period или timeframe текущего графика (для текущего графика — предопределенная переменная _Period или функция Period()).
При количестве баров на графике менее 60, наш советник не будет работать и выйдет из функции OnTick. Функция Alert показывает сообщение в отдельном окне. Эта функция выводит значения аргументов/параметров, разделенных запятыми. В нашем случае выводится только одно значение в виде строки и завершается работа функции OnTick.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Достаточно ли количество баров для работы if(Bars(_Symbol,_Period)<60) // общее количество баров на графике меньше 60? { Alert("На графике меньше 60 баров, советник не будет работать!!"); return; } // Для сохранения значения времени бара мы используем static-переменную Old_Time. // При каждом выполнении функции OnTick мы будем сравнивать время текущего бара с сохраненным временем. // Если они не равны, это означает, что начал строится новый бар. static datetime Old_Time; datetime New_Time[1]; bool IsNewBar=false; // копируем время текущего бара в элемент New_Time[0] int copied=CopyTime(_Symbol,_Period,0,1,New_Time); if(copied>0) // ok, успешно скопировано { if(Old_Time!=New_Time[0]) // если старое время не равно { IsNewBar=true; // новый бар if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("Новый бар",New_Time[0],"старый бар",Old_Time); Old_Time=New_Time[0]; // сохраняем время бара } } else { Alert("Ошибка копирования времени, номер ошибки =",GetLastError()); ResetLastError(); return; } //--- советник должен проверять условия совершения новой торговой операции только при новом баре if(IsNewBar==false) { return; } //--- Имеем ли мы достаточное количество баров на графике для работы int Mybars=Bars(_Symbol,_Period); if(Mybars<60) // если общее количество баров меньше 60 { Alert("На графике менее 60 баров, советник работать не будет!!"); return; } //--- Объявляем структуры, которые будут использоваться для торговли MqlTick latest_price; // Будет использоваться для текущих котировок MqlTradeRequest mrequest; // Будет использоваться для отсылки торговых запросов MqlTradeResult mresult; // Будет использоваться для получения результатов выполнения торговых запросов MqlRates mrate[]; // Будет содержать цены, объемы и спред для каждого бара ZeroMemory(mrequest); // Инициализация полей структуры mrequest
Наш советник должен производить торговые операции только при начале нового бара, поэтому нужно решить задачу определения факта появления нового бара. Другими словами, советник не будет работать на каждом тике, проверка условий и торговля будет производиться только после окончания формирования бара.
Мы начнем с объявления статической переменной Old_Time, в которой будем хранить время бара. Мы определили ее статической, поскольку нам нужно, чтобы ее значение сохранялось при новом вызове функции. Тогда у нас будет возможность проверять ее значение с переменной New_Time, которая также объявлена типа datetime, но в виде массива из одного элемента, она будет использоваться для хранения времени текущего бара. Также мы объявляем переменную IsNewBar типа boolean, и устанавливаем ее значение в false. Ее значение будет установлено в true только в случае определения факта появления нового бара.
Для получения времени бара используется функция CopyTime. Она копирует время бара в массив New_Time, состоящий из одного элемента. В случае успеха, мы сравниваем значение времени бара с сохраненным ранее временем предыдущего бара. Если они различны, это означает, что появился новый бар и переменная IsNewBar устанавливается в true, а значение текущего времени бара сохраняется в переменной Old_Time.
Таким образом, переменная IsNewBar будет указывать на факт появления нового бара. Если ее значение равно false, мы завершаем выполнение функции OnTick.
Обратите внимание на строчку:
if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("Новый бар",New_Time[0],"старый бар",Old_Time);
здесь проверяется исполнение советника в режиме отладки, если он запущен в отладчике, будет выводится сообщение о значениях времен баров, режим отладки мы рассмотрим позже.
Следующее, что мы собираемся cделать — проверить наличие достаточного количества баров для работы. Зачем делать это снова?
Мы хотим быть уверены в том, что наш советник работает корректно.
Следует отметить, что функция OnInit вызывается только один раз при присоединении советника к графику, а функция OnTick вызывается каждый раз при поступлении нового тика (ценовой котировки).
Как можно видеть, мы это делаем по-другому. Мы сохраняем общее количество баров в истории в новой переменной Mybars, определенной внутри функции OnTick:
int Mybars=Bars(_Symbol,_Period);
Этот тип переменной является локальной переменной, в отличие от переменных, декларированных в разделе входных параметров нашего кода.
Глобальные переменные доступны для всех функций советника, локальные переменные определяются внутри функций, их видимость ограничена лишь функцией, внутри которой они декларированы. Они не могут быть использованы вне функции.
Далее мы определили несколько переменных типа структур MQL5, которые будут использованы в данном разделе нашего советника. В языке MQL5 есть множество готовых структур, что значительно облегчает жизнь разработчикам советников. Давайте последовательно рассмотрим их.
MqlTick
Эта структура используется для хранения последних цен по символу.
struct MqlTick
{
datetime time; // Время последнего обновления цен
double bid; // Текущая цена Bid
double ask; // Текущая цена Ask
double last; // Текущая цена последней сделки (Last)
ulong volume; // Объем для текущей цены Last
};
Любая переменная, объявленная типа может быть легко использована для получения текущих значений цен Ask, Bid, Last и Volume, достаточно вызвать функцию SymbolInfoTick.
Мы объявили переменную latest_price как структуру MqlTick, так что мы можем использовать ее для получения цен Bid и Ask.
MqlTradeRequest
Эта структура используется в запросах на проведение торговых операций. Она содержит все поля, необходимые для заключения торговых сделок.
struct MqlTradeRequest
{
ENUM_TRADE_REQUEST_ACTIONS action; // Тип выполняемого действия
ulong magic; // Идентификатор magic number эксперта
ulong order; // Тикет ордера
string symbol; // Имя торгового инструмента
double volume; // Запрашиваемый объем сделки в лотах
double price; // Цена
double stoplimit; // Уровень StopLimit ордера
double sl; // Уровень Stop Loss ордера
double tp; // Уровень Take Profit ордера
ulong deviation; // Максимально приемлемое отклонение от запрашиваемой цены
ENUM_ORDER_TYPE type; // Тип ордера
ENUM_ORDER_TYPE_FILLING type_filling; // Тип ордера по исполнению
ENUM_ORDER_TYPE_TIME type_time; // Тип ордера по времени действия
datetime expiration; // Срок истечения ордера (для ордеров типа ORDER_TIME_SPECIFIED)
string comment; // Комментарий к ордеру
};
Любая переменная, объявленная как структура MqlTradeRequest может быть использована для отправки запросов на совершение торговых операций. В нашем случае мы объявили переменную mrequest как структуру MqlTradeRequest.
MqlTradeResult
Результат выполнения торговой операции возвращается в специальную предопределенную структуру типа MqlTradeResult. Любая переменная типа MqlTradeResult может быть использована для доступа к результату выполнения торгового запроса.
struct MqlTradeResult
{
uint retcode; // Код результата операции
ulong deal; // Тикет сделки, если она совершена
ulong order; // Тикет ордера, если он выставлен
double volume; // Объем сделки, подтверждённый брокером
double price; // Цена в сделке, подтверждённая брокером
double bid; // Текущая рыночная цена Bid
double ask; // Текущая рыночная цена Ask
string comment; // Комментарий брокера к операции (по умолчанию заполняется расшифровкой)
};
В нашем случае переменная mresult объявлена как структура тип MqlTradeResult.
MqlRates
Цены (Open, Close, High, Low), время, объем каждого бара, и спред символа хранятся в этой структуре. Любой массив, определенный как массив типа MqlRatesможет быть использован для хранения значений цен, объемов и спредов по символу.
struct MqlRates
{
datetime time; // Время начала периода
double open; // Цена открытия
double high; // Наивысшая цена за период
double low; // Наименьшая цена за период
double close; // Цена закрытия
long tick_volume; // Тиковый объем
int spread; // Спред
long real_volume; // Биржевой объем
};
Для наших целей мы определили массив mrate[], который будет использоваться для хранения этой информации.
/*