English Español Deutsch 日本語 Português
preview
Изучение MQL5 — от новичка до профи (Часть VI):  Основы написания советников

Изучение MQL5 — от новичка до профи (Часть VI): Основы написания советников

MetaTrader 5Примеры |
2 158 3
Oleh Fedorov
Oleh Fedorov

Введение

Вот мы и добрались до создания советников. Можно сказать, рубикон перейден.

Для того, чтобы эта статья была вам полезна, вам необходимо легко оперировать такими концепциями, как:

  • переменные (локальные и глобальные), 
  • функции и их параметры (переданные по ссылке и по значению), 
  • массивы (включая понимание массивов-серий),
  • основные операторы, включая логические и арифметические, а также условные операторы (выбор, ветвление, тернарный) и операторы цикла (преимущественно for, но лишними не будут и while, do... while).

С точки зрения программиста, советники не намного сложнее индикатора, рассмотренного в прошлой статье цикла. Для торговли всё так же нужно проверить условия для сделки, и, если эти условия наступили, выполнить какие-то действия (обычно отправить торговый приказ на сервер). Главное — понимать структуру самих приказов и знать функции для отправки этих приказов, а также для получения нужных для торговли данных. 

Внимание! Все советники, приведённые в данной статье, предназначены только для иллюстрации принципов программирования, а не для получения прибыли. Если вы будете использовать этот код на реальных счетах, вероятно, алгоритмы принятия решений о торговле должны быть доработаны, иначе можно получить убытки.

Правда, код советников из этой статьи нельзя напрямую использовать для реальной торговли, даже если улучшить алгоритмы входа. Дело в том, что в первом примере советника не проверяются никакие ошибки: ни для запроса, ни для ответа. Это сильно упрощает восприятие кода, что нам сейчас и нужно, но делает советник пригодными лишь для быстрого прототипирования и проверки работоспособности стратегий. Во втором советнике проверок больше... Но для публикации на Маркете или даже для надёжной торговли — когда возникающие проблемы решаются по мере их поступления (если вообще возможно решить данный тип проблем, а не просто сообщить о нём и корректно завершить работу) — этих проверок тоже недостаточно.

Полностью работоспособный советник, который технически могут принять на  Маркете, включающий необходимые проверки и немного усложнённый по сравнению с данной статьёй алгоритм работы, мы разберём в следующей статье. Здесь же я хочу привести лишь основы торговли. Будут написаны два советника: безиндикаторный и использующий встроенный индикатор скользящей средней. Причём первый из них будет торговать отложенными ордерами, второй — совершать сделки по текущей цене.



Шаблон советника

Любой советник начинается с того, что программист создаёт пустой шаблон, например, с помощью мастера создания файлов (рисунок 1).

Первое окно мастера создания файлов

Рисунок 1. Первое окно мастера создания файлов

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

При генерации создаётся объектно-ориентированный вариант советника, разложенный по нескольким файлам, и разобраться в нём неопытному человеку без дополнительных инструментов  будет довольно сложно, а создать свой набор инструкций для торговли на его основе — ещё сложнее. Поэтому нижний вариант рекомендую использовать только после того, как вы очень хорошо поймёте концепции и практики ООП, не раньше. Разобраться в сгенерированном мастером коде будет, вероятно, интересно с точки зрения "посмотреть, как можно", но это — только одна из возможных реализаций, удобная для автоматического использования. Возможно, что к тому моменту, когда у вас появится опыт, позволяющий разобраться в хитросплетениях классов этой реализации, вы подготовите свои шаблоны кода. Понятно, что свой код обычно намного удобнее редактировать, чем разбираться в чужом. И вполне вероятно, что ваши шаблоны будут не хуже того, что предлагает мастер.

Большинство функций, которые может создавать мастер (рисунки 2, 3), являются необязательными, но часто бывают очень полезны. Так, с помощью функций третьего окна мастера (рисунок 2) можно обработать события, возникающие при проведении сделки, такие как получение сигнала торговым сервером, установка позиции и т.д. (OnTrade и OnTradeTransaction), а также обрабатывать события таймера (OnTimer), события графика вроде нажатия кнопок или создания нового графического объекта (OnChartEvent) и события изменения стакана цен (OnBookEvent).

Создание советника - третий экран мастера

Рисунок 2. Создание советника — третий экран мастера (дополнительные функции советника).

Иногда можно также использовать специальные функции, которые обрабатываются только в тестере стратегий, но не в обычном режиме (рисунок 3). Задачи, когда может быть полезно разделять эти два режима, чаще всего связаны с демонстрационными версиями, которые работают только в тестере, но не на обычных счетах. Впрочем, иногда для тестера надо выводить более подробные логи или брать данные из других источников, не тех, что при обычной торговле. В общем, хоть эти функции я использую редко, они всё же бывают полезны.

Функции советника для рабты в тестере

Рисунок 3. Создание советника — четвёртый экран мастера (функции советника для работы только  в тестере).

Некоторые функции с рисунка 2 мы подробно рассмотрим в следующих статьях, а вот функции с рисунка 3 полностью оставляю вам на самостоятельную проработку.

При создании советника с помощью мастера всегда создаётся файл, содержащий минимум три функции (пример 1):

//+------------------------------------------------------------------+
//|                                                  FirstExpert.mq5 |
//|                                       Oleg Fedorov (aka certain) |
//|                                   mailto:coder.fedorov@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Oleg Fedorov (aka certain)"
#property link      "mailto:coder.fedorov@gmail.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Пример 1. Минимальный шаблон советника, создаваемого мастером

  • OnInit — знакомая по индикаторам функция предварительной подготовки, запускается один раз при старте нашей программы;
  • OnDeinit — тоже должна быть вам знакома, она срабатывает при завершении работы советника. Напомню, эта функция нужна для того, чтобы удалить все уже ненужные объекты, созданные советником во время работы, а также выполнить остальные завершающие действия вроде закрытия файлов, освобождения ресурсов индикаторов — и тому подобное;
  • OnTick - выполняется на каждом тике (и этим подобна функции OnCalculate в индикаторе). В ней происходит основная работа.

Обязательной для советника функцией является только OnTick, а OnInit и OnDeinit можно не использовать, если ваш советник простой.



Основные термины, которые необходимо понимать при торговле в MetaTrader 5

При автоторговле в MetaTrader 5  разработчики используют определённые термины. С этими терминами связана логика торговли, с ними же связаны названия функций. Попробуем с ними разобраться.

Order (приказ) — сообщение серверу о нашем желании купить или продать определённый инструмент по определённой цене.

Все без исключения изменения в торговле — будь то немедленная покупка или изменение цены Stop Loss — происходят с помощью торговых приказов. Напомню, приказы бывают для немедленного исполнения (например, приказ купить или продать по текущей цене) и "отложенные", предполагающие, что цена сделки еще не наступила, но наступит в приемлемый для нашего алгоритма период времени (стоп или лимит ордера).

К отложенным относятся приказы Stop Loss, Take Profit, а также Buy/Sell Stop, Buy/Sell Limit, Buy/Sell Stop Limit. Примеры названий функций, работающих с приказами: OrderSend (отправляет приказ на сервер), OrderGetInteger (возвращает целое число — параметр данного ордера, например, ярлык (ticket) или время создания).

Deal (сделка) — момент исполнения приказа.

Сделка в MetaTrader 5 — явление скорее историческое. Мы не можем на неё никак повлиять, поскольку происходит она на сервере, когда приказ уже отдан. Но мы можем посмотреть в истории, когда именно она была совершена и по какой цене. Примеры названий функций: HistoryDealGetDouble (позволяет получить какой-нибудь параметр сделки типа double, например, цену), HistoryDealsTotal (возвращает общее количество сделок в истории).

Position (позиция) — итоговое содержимое вашего портфеля после всех сделок по конкретному инструменту.

Изначально задумывалось, что в  MetaTrader 5  по одному символу может быть только одна позиция, но исторически сложилась чуть более сложная картина. Сейчас, в зависимости от типа счёта, открытого трейдером, все сделки либо изменяют только одну позицию (netting), либо каждая сделка создаёт свою позицию (hedging), если это не сделка по стоп-приказу. В итоге на счетах типа hedging может получиться несколько позиций даже по одному инструменту, иногда даже одновременно в разные стороны. В этом случае, при необходимости можно вычислить совокупную позицию на покупку и на продажу. С помощью торговых приказов позиции можно изменять: закрывать полностью или частично, изменять их уровни Stop Loss и Take Profit. Примеры названий функций, работающих с позициями: PositionSelectByTicket (выбрать позицию по её ярлыку), PositionGetString (получить какой-нибудь строковый параметр, например, имя символа или комментарий, заданный в момент открытия этой позиции).

Каждая сделка является результатом исполнения какого-то торгового приказа, каждая позиция является итоговым результатом одной или нескольких сделок.

Event (событие) — это любое значимое изменение, которое происходит в окружении нашей программы.

Отправили торговый запрос — событие. Сервер принял запрос, пользователь щелкнул мышкой на графике, график изменил масштаб, сервер прислал уведомление о том, что запрос обработан, пришел новый тик... Всё это и многое другое — тоже события. Для обработки наиболее значимых из них введены стандартные функции-обработчики, имя которых начинается на "On" — типа OnInit (выполняется по событию Init) или OnTick (обрабатывает событие Tick).

Есть и другие события, которые обрабатываются просто с помощью списка констант. Это значит, что сначала должна сработать одна из функций-обработчиков глобального события. Например, функция OnChartEvent срабатывает на любое событие графика. И уже внутри этой функции для того, чтобы понять, какое именно событие произошло, нужно сравнить переменную, отвечающую за приём событий, с эталонным значением. Обычно данные для идентификации событий передаются таким функциям в виде параметров. В этой статье такие "мелкие" события рассматриваться не будут.


Основные принципы автоматической торговли в терминале MetaTrader 5

Бросим взгляд на процесс торговли "с высоты птичьего полёта". И начнём с очевидного факта, имеющего важные следствия.

Программа терминала, которая стоит на вашем компьютере, и программа, реально оперирующая вашими деньгами (сервер) — это две разные программы. Важно, что общаться они могут только через сеть (как правило, это интернет).

Это значит, что когда вы нажимаете кнопку "купить" или "продать", происходит несколько последовательных событий (event):

  • ваш терминал формирует особый пакет данных, заполняя специальную структуру  MqlTradeRequest;
  • затем заполненная структура отправляется на сервер с помощью функции OrderSend (обычная синхронная отправка) или OrderSendAsinc (асинхронный режим), формируя приказ (order) о сделке;
  • сервер получает этот пакет и проверяет его данные на соответствие реальным возможностям: есть ли в предложениях от других трейдеров цена, соответствующая вашим требованиям, достаточно ли у вас средств для сделки, и т.д.;
  • если всё в порядке, приказ размещается в очереди среди всех других приказов (от других трейдеров) и ожидает выполнения (прихода нужной цены); 
  • об этом отправляется сообщение на терминал;
  • если цена достигла нужного уровня, сервер совершает сделку (deal) и записывает информацию о ней в свой журнал;
  • после совершения сделки сервер отправляет терминалу результаты своей работы;
  • терминал получает результирующее сообщение сервера, которое будет записано в структуру MqlTradeResult, и в результате этого будут сгенерированы события терминала Trade и TradeTransaction;
  • затем терминал должен проверить, не произошло ли ошибок на стороне сервера (программист делает это, проверяя поле retcode структуры MqlTradeRequest),
  • и, если всё в порядке, терминал обновит свои переменные, а также — журнал и графическое изображение произошедших событий;
  • в итоге у нас появляется (или обновляется) позиция (position) по какому-то инструменту.

Если этот список представить в виде несколько упрощённой схемы, получим изображение на рисунке 4:

Процесс торговли распределён между терминалом и сервером

Рисунок 4. Схема обработки торгового приказа


Асинхронный режим передачи данных

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

Однако передача данных по сети — это очень длительная с точки зрения процессора операция. Если выполнение скрипта для торговли длится обычно несколько сот микросекунд (скажем, 200 мкс = 2е-4 с), то передача данных по сети обычно измеряется в миллисекундах (допустим, 20 мс = 2e-2 с), то есть минимум в 100 раз дольше. Плюс надо учитывать время работы сервера. Плюс изредка на сервере может случиться техобслуживание или другая внештатная ситуация... В общем, в особо неудачных случаях между отправкой торгового приказа и получением ответа могут пройти секунды и даже минуты. Если всё это время советник будет ожидать ответа, и таких советников будет несколько десятков... Много процессорного времени в этом случае будет расходоваться непродуктивно.

Поэтому для получения ответов сервера разработчики добавили в MetaTrader специальный асинхронный режим. Слово "асинхронный" означает, что советник может послать приказ и не ждать ответа, а заниматься дальше своими делами, например, просто "спать" или заниматься какими-то сложными вычислениями. Когда придёт ответ сервера, терминал сгенерирует событие TradeTransaction (а затем и Trade), и советник сможет активизироваться снова и использовать этот ответ для принятия дальнейших торговых решений. Преимущества такого подхода очевидны.

И в синхронном, и в асинхронном режиме для обработки ошибок торговли используется функция OnTradeTransaction. Сложность кода при этом не повышается, просто часть функций переносится из OnTick в OnTradeTransaction. А если вынести этот  код в отдельные функции, то вызов и перенос его вообще не вызовет проблем. Поэтому, какой из режимов использовать в торговле для конкретных задач, программист может выбрать сам. Структуры данных для этих режимов одинаковы.


Начинаем торговать

Допустим, нам нужен эксперт для FOREX, который ищет внутренний бар. Напомню, внутренним баром считается бар, диапазон которого находится полностью внутри диапазона предыдущего бара. Эксперт срабатывает один раз за свечу, и, если находит нужный шаблон, отправляет одновременно два отложенных приказа:

  • на покупку, устанавливая его цену на пару пунктов (можно настроить) выше максимума большей свечи,
  • и на продажу, устанавливая  цену на таком же расстоянии ниже минимума этой же свечи,
  • время жизни приказа равно двум свечам, если за это время приказ не сработал, он удаляется,
  • защитные приказы (Stop Loss) для обоих ордеров выставляются на середину большой свечи, 
  • приказы о взятии прибыли (Take Profit) будем ставить на отметке 7/8 длины большой свечи,
  • объём сделки вычисляем как минимально возможный лот.

В данной версии советника для более простого восприятия кода других условий не будет. Напишем "каркас", который позже можно будет улучшать. Для начала создадим шаблон советника, оставив пустыми все галочки в третьем окне (в данной статье мы не будем обрабатывать ответы сервера). В этом случае код заготовки будет аналогичен коду, приведённому в примере 1. И для того, чтобы можно было настраивать наш советник и иметь возможность его оптимизировать, добавим четыре параметра: на каком расстоянии от экстремумов ставить ордера (inp_PipsToExtremum), на каком расстоянии ставить стоп и профит приказы (inp_StopCoeffcient и inp_TakeCoeffcient соответственно) и сколько баров должно пройти для отмены не сработавшего ордера (inp_BarsForOrderExpired). Кроме того, объявим магический номер нашего эксперта, чтобы в будущем отличать "свои" торговые приказы от "чужих".

//--- объявление и инициализация входных параметров
input int     inp_PipsToExtremum      = 2;
input double  inp_TakeCoeffcient      = 0.875;
input double  inp_StopCoeffcient      = 0.5;
input int     inp_BarsForOrderExpired = 2;

//--- объявление и инициализация глобальных переменных
#define EXPERT_MAGIC 11223344

Пример 2. Описание входных параметров и магического номера советника

На всякий случай напомню, что код примера 2 надо будет вставить в самом верху файла советника, сразу после директив #property.

Остальной код в данном примере мы сосредоточим внутри функции OnTick. Все другие функции сейчас оставим пустыми. Вот этот код, который нужно поместить в тело OnTick:

 /****************************************************************
  *    Учитывайте, что в этом советнике для доступа к данным     *
  * будут использованы стандартные функции. Следовательно, все   *
  * массивы (времени и цен) удобно использовать в виде серий.    *
  ****************************************************************/
  string          symbolName  = Symbol();
  ENUM_TIMEFRAMES period      = PERIOD_CURRENT;

//--- Определяем новую свечу (Работаем только в начале новой свечи)
  static datetime timePreviousBar = 0; // Время предыдущей свечи
  datetime timeCurrentBar;             // Время текущей свечи

  // Получаем время текущей свечи с помощью стандартной функции
  timeCurrentBar = iTime(
                     symbolName, // Имя символа
                     period,     // Период
                     0           // Номер свечи (помним о сериях)
                   );

  if(timeCurrentBar==timePreviousBar)
   {
    // Если время текущей и предыдущей свечей совпадают
    return;  // Выходим из функции и ничего не делаем
   }
  // Иначе текущая свеча становится предыдущей,
  //   чтобы на следующем тике не торговать
  timePreviousBar = timeCurrentBar;

//--- Подготовка данных для торговли)
  double volume=SymbolInfoDouble(symbolName,SYMBOL_VOLUME_MIN); // Объём (лотов) - получаем минимально возможный объём

  // Экстремумы свечей.
  double high[],low[]; // Объявляем массивы

  // Объявляем, что массивы являются сериями
  ArraySetAsSeries(high,true);
  ArraySetAsSeries(low,true);

  // Заполняем массивы значениями первых двух закрытых свечей
  //   (начинаем копирование с номера 1, так как нужны 
  //   только закрытые свечи; берём 2 значения)
  CopyHigh(symbolName,period,1,2,high);
  CopyLow(symbolName,period,1,2,low);


  double lengthPreviousBar; // Размах диапазона "длинного" бара
  MqlTradeRequest request;  // Структура запроса
  MqlTradeResult  result;   // Структура ответа сервера

  if( // Если первый закрытый бар - внутренний
    high[0]<high[1]
    && low[0]>low[1]
  )
   {
    // Вычисляем размах диапазона
    lengthPreviousBar=high[1]-low[1];  // Напомню, в сериях нумерация справа налево

  //--- Подготавливаем данные для приказа на покупку
    request.action      =TRADE_ACTION_PENDING;                         // тип ордера (отложенный)
    request.symbol      =symbolName;                                   // имя символа
    request.volume      =volume;                                       // объем сделки
    request.type        =ORDER_TYPE_BUY_STOP;                          // действие ордера (покупка)
    request.price       =high[1] + inp_PipsToExtremum*Point();         // цена покупки
    // Необязательные параметры
    request.deviation   =5;                                            // допустимое отклонение от цены
    request.magic       =EXPERT_MAGIC;                                 // магический номер советника
    
    request.type_time   =ORDER_TIME_SPECIFIED;                         // Параметр обязателен, чтобы устанавливать время жизни
    request.expiration  =timeCurrentBar+
                         PeriodSeconds()*inp_BarsForOrderExpired;      // Время жизни ордера

    request.sl          =high[1]-lengthPreviousBar*inp_StopCoeffcient;  // Stop Loss
    request.tp          =high[1]+lengthPreviousBar*inp_TakeCoeffcient;  // Take Profit


  //--- Отправляем ордер на покупку на сервер
    OrderSend(request,result); // Для асинхронного режима нужно использовать OrderSendAsync(request,result);
    
  //--- Очищаем структуры запроса и ответа для повторного использования
    ZeroMemory(request);
    ZeroMemory(result);
    
  //--- Готовим данные для приказа на продажу. Параметры те же, что и в предыдущей функции.
    request.action      =TRADE_ACTION_PENDING;                         // тип ордера (отложенный)
    request.symbol      =symbolName;                                   // имя символа
    request.volume      =volume;                                       // объем
    request.type        =ORDER_TYPE_SELL_STOP;                         // действие ордера (продажа)
    request.price       =low[1] - inp_PipsToExtremum*Point();          // цена покупки
    // Необязательные параметры
    request.deviation   =5;                                            // допустимое отклонение от цены
    request.magic       =EXPERT_MAGIC;                                 // магический номер советника
    
    request.type_time   =ORDER_TIME_SPECIFIED;                         // Параметр обязателен, чтобы устанавливать время жизни
    request.expiration  =timeCurrentBar+
                         PeriodSeconds()*inp_BarsForOrderExpired;      // Время жизни ордера

    request.sl          =low[1]+lengthPreviousBar*inp_StopCoeffcient;   // Stop Loss
    request.tp          =low[1]-lengthPreviousBar*inp_TakeCoeffcient;   // Take Profit

  //--- Отправляем ордер на продажу на сервер
    OrderSend(request,result);
   }
 

Пример 3. Функция OnTick данного советника содержит всю логику торговли

Стандартная функция Point возвращает размер пункта для текущего графика. Так, если провайдер даёт пятизначные котировки, для пары EURUSD пункт будет равен 0.00001, а для USDJPY в этом случае он будет 0.001. Функции iTime, iHigh, iLow позволяют получить, соответственно, время, наибольшее и наименьшее значение цены на конкретной свече (по номеру свечи, начиная справа). В данном примере мы использовали только iTime для получения текущего времени при проверке смены бара. Для получения значений High и Low были использованы функции копирования массивов CopyHigh и CopyLow.

Код имеет два блока: проверка новой свечи и торговля (которая начинается со стадии подготовки). Блок торговли, в свою очередь, разделяется на два почти идентичных блока: для покупки и для продажи. Понятно, что заполнение структуры и отправка запроса настолько похожи в обоих случаях, что можно было бы создать отдельную функцию, которая заполняет общие поля и "ветвится" только при заполнении специфических полей, таких как тип приказа и цены исполнения (price, tp, sl). Но в этом примере универсальность и компактность кода принесены в жертву наглядности.

Каждую стадию процесса я выделил комментарием "//---", а внутри каждой стадии комментарии обычные, не имеющие трёх дефисов. Торговля состоит из двух частей: заполнение структуры запроса и отправка этой структуры, причём при заполнении структуры обязательными являются только первые пять полей.

Важно отметить, что если вам необходимо использовать время истечения ордера, как сделано в данном примере, вы должны заполнить два поля: request.type_time и собственно request.expiration. Если не заполнить первое поле, второе по умолчанию будет проигнорировано.

Чтобы посмотреть, как будет работать этот советник, можно запустить его на любом интервале на демо счете (на минутках тоже работает, хотя, конечно, это зависит от спрэда по символу, на котором вы будете запускать советник) или нажать в MetaEditor  <Ctrl>+<F5>, что запустит отладку на исторических данных в тестере стратегий. Полный текст примера во вложенном файле TrendPendings.mq5.


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

Советник из примера 3 не использовал индикаторов, однако так бывает не всегда. Для стратегий, основанных на стандартных индикаторах, у нас есть два пути: использовать стандартные функции или использовать классы, описывающие данные индикаторы. Рассмотрим оба варианта, и начнём со встроенных функций.

Пусть нам нужно создать советник на основе скользящей средней. Я снова напомню, что в этой статье я не создаю прибыльные советники, я демонстрирую принципы  работы, поэтому хочу максимально упростить код и сделать его настолько читабельным, насколько смогу. Имея это в виду, определим правила торговли:

  • торгуем, как и в предыдущем советнике, только при образовании новой свечи;
  • для покупки предыдущая свеча должна закрыться выше скользящей средней; 
  • для продажи предыдущая свеча должна закрыться ниже скользящей средней;
  • пусть фильтром будет наклон средней: если от второй свечи (предпоследней) до первой (самой правой закрытой) средняя растёт, значит, покупаем, если падает - продаём;
  • выход из позиции - по противоположному сигналу;
  • защитный приказ ставим на максимум (для продажи) или минимум (для покупки) сигнальной свечи;
  • по символу может быть только одна позиция, даже если счёт хеджинговый; если пришёл сигнал, но позиция уже есть - не торгуем.

На рисунке 5 показан принцип фильтрации, используемый данным советником.

Принуип отбора сигнальных свечей

Рисунок 5. Принцип отбора свечей в советнике для торговли по скользящим средним

В данном советнике я по-прежнему буду стараться сделать код максимально наглядным, однако  буду использовать больше проверок на ошибки, чтобы немного приблизить программу к реальным образцам.

Во входные параметры вынесем все параметры средней, а также — максимально допустимое отклонение цены от цены запроса. Кроме того объявим глобальную переменную для хэндла индикатора (объясню чуть ниже) и опишем магический номер нашего советника:

//--- объявление и инициализация глобальных переменных

#define EXPERT_MAGIC 3345677

input int inp_maPeriod = 3;                                 // Период средней
input int inp_maShift = 0;                                  // Сдвиг
input ENUM_MA_METHOD inp_maMethod = MODE_SMA;               // Режим вычислений
input ENUM_APPLIED_PRICE inp_maAppliedPrice = PRICE_CLOSE;  // Цена, к которой применяется расчёт
input int inp_deviation = 5;                                // максимальное отклонение от цены запроса, пунктов

//--- Хэндл индикатора средней
int g_maHandle;

Пример 4. Глобальные переменные советника для торговли по скользящим средним

Прежде, чем использовать любой индикатор в советнике или другом индикаторе, нам нужно выполнить три действия:

  1. Инициализировать индикатор и получить на него ссылку. Для этих целей обычно используются либо функции для встроенных индикаторов (например, iMA для скользящей средней), либо iCustom для индикаторов, написанных пользователями. Обычно эту процедуру мы совершаем в функции OnInit.

    В английском языке слово handle имеет два перевода: "рукоять" (с помощью которой можно, например, держать инструмент или нести чемодан) и "псевдоним", способ обратиться. В общем, нечто, что позволяет "справиться" с нужным нам объектом. В русском я не смог подобрать точного аналога, поэтому буду либо говорить о "ссылке на индикатор", либо просто транслитерировать это слово как хэндл.
  2. Перед каждым новым использованием нужно прочитать новые данные. Для этого обычно заводится по одному массиву на индикаторный буфер, а затем они заполняются с помощью функции CopyBuffer.
  3. Использовать данные из заполненных массивов.
  4. При завершении работы нашей программы все хэндлы индикаторов нужно освободить, чтобы не было утечек памяти. Для этого обычно используется функция IndicatorRelease, и применяется она, как несложно догадаться, внутри функции OnDeinit.

Функции OnInit и OnDeinit в данном советнике ничего особенного из себя не представляют:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
 {
//--- Перед действием, потенциально способным вызвать ошибку, сбрасываем
//   встроенную переменную _LastError в исходное состояние
//   (предполагаем, что ошибки пока нет)
  ResetLastError();

//--- Стандартная функция iMA возвращает хэндл индикатора
  g_maHandle = iMA(
                 _Symbol,           // Символ
                 PERIOD_CURRENT,    // Период графика
                 inp_maPeriod,      // Период средней
                 inp_maShift,       // Сдвиг средней
                 inp_maMethod,      // Метод расчёта средней
                 inp_maAppliedPrice // К чему применить расчёт
               );
// inp_maAppliedPrice в общем случае может быть
// либо обозначением цены, как в этом примере,
// (из перечисления ENUM_APPLIED_PRICE),
// либо хэндлом другого индикатора

//--- если не удалось создать хэндл
  if(g_maHandle==INVALID_HANDLE)
   {
    //--- сообщим о неудаче и выведем код ошибки
    PrintFormat("Не удалось создать ссылку на индикатор iMA для пары %s/%s, код ошибки %d",
                _Symbol,
                EnumToString(_Period),
                GetLastError() // Выводим код ошибки
               );
    //--- При ошибке работа советника завершается досрочно
    return(INIT_FAILED);
   }
//---
  return(INIT_SUCCEEDED);
 }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
 {
//--- Освобождаем ресурсы, занятые индикатором
  if(g_maHandle!=INVALID_HANDLE)
    IndicatorRelease(g_maHandle);
 }

Пример 5. Инициализация и деинициализация индикаторов в советнике

Нюанс, на который хочу обратить ваше внимание — дуэт функций  ResetLastError и GetLastError в функции инициализации. Первая сбрасывает стандартную переменную _LastError в состояние "нет ошибки", а с помощью второй можно получить код последней ошибки, если она всё-таки случилась.

А в остальном всё тривиально. Функции инициализации индикаторов (в том числе iMA) возвращают либо хэндл индикатора, либо специальное значение INVALID_HANDLE (неверный хэндл). Благодаря этому мы можем заметить, если что-то пошло не так и обработать ошибку (в нашем случае — вывести сообщение). Если OnInit возвращает INIT_FAILED, советник (или индикатор) не запустится. Собственно, если нам не удалось получить ссылку на индикатор средней, прекратить работу —  единственное правильное решение.

А вот в коде OnTick будем разбираться по частям. Часть первая — объявление и инициализация переменных.

//--- Объявление и инициализация переменных
  MqlTradeRequest requestMakePosition;  // Структура запроса на создание новой позиции
  MqlTradeRequest requestClosePosition; // Структура запроса на закрытие существующей позиции
  MqlTradeResult  result;               // Структура для ответа сервера
  MqlTradeCheckResult checkResult;      // Структура для проверки запроса перед отправкой

  bool positionExists = false;      // Флаг существования позиции
  bool tradingNeeds = false;        // Флаг разрешения торговли
  ENUM_POSITION_TYPE positionType;  // Тип открытой позиции
  ENUM_POSITION_TYPE tradingType;   // Тип нужной нам позиции (для сравнения с открытой)
  ENUM_ORDER_TYPE orderType;        // Нужный нам тип торгового приказа
  double requestPrice=0;            // Цена входа в будущую позицию

  /* Структура MqlRates (котировки) содержит 
     все данные свечи: цены открытия, закрытия, 
     максимума и минимума, тиковый объём, 
     реальный объём, спред и время.
     
     В данном примере я решил продемонстрировать, как заполнять её сразу, 
     а не получать каждую величину по отдельности.                              */
  
  MqlRates rates[];   // Массив котировок для проверки торговых условий
  double maValues[];  // Массив значений скользящей средней

// Описываем массивы данных как серии
  ArraySetAsSeries(rates,true);
  ArraySetAsSeries(maValues,true);

Пример 6. Локальные переменные функции OnTick

Проверка нового бара здесь никак не меняется:

//--- Проверяем, что пришел новый бар
  static datetime previousTime  = iTime(_Symbol,PERIOD_CURRENT,0);
  datetime currentTime          = iTime(_Symbol,PERIOD_CURRENT,0);
  if(previousTime==currentTime)
   {
    return;
   }
  previousTime=currentTime;

Пример 7. Проверка нового бара

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

//---  Готовим данные для обработки
// Копируем котировки двух баров, начиная с первого
  if(CopyRates(_Symbol,PERIOD_CURRENT,1,2,rates)<=0)
   {
    PrintFormat("Ошибка данных по символу %s, код ошибки %d", _Symbol, GetLastError());
    return;
   }

// Копируем значения индикаторного буфера скользящей средней
  if(CopyBuffer(g_maHandle,0,1,2,maValues)<=0)
   {
    PrintFormat("Ошибка при получении данных индикатора, код ошибки %d", GetLastError());
    return;
   }

 Пример 8. Копирование текущих данных индикатора и котировок в локальные массивы

И теперь выбираем открытую позицию по текущему символу с помощью стандартной функции PositionSelect. В правилах мы договорились, что позиция по символу может быть только одна, так что проблем возникнуть не должно, но мы всё равно тщательно продумываем, что могло бы пойти не так... Как минимум нужно проверить, что позиция открыта нашим советником:

//--- Определяем, есть ли открытая позиция
  if(PositionSelect(_Symbol))
   {
    // Устанавливаем флаг открытой позиции - для дальнейшей обработки
    positionExists = true;
    // Сохраняем тип открытой позиции
    positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

    // Проверяем, нашим ли советником открыта данная позиция
    requestClosePosition.magic = PositionGetInteger(POSITION_MAGIC); // отдельной переменной для существующего магического 
                                                                     // номера  позиции я не создавал
    if(requestClosePosition.magic!= EXPERT_MAGIC)
     {
      // Еще какой-то советник начал торговать на нашем символе. Не будем ему мешать...
      return;
     } // if(requestClosePosition.magic!= EXPERT_MAGIC)
   } // if(PositionSelect(_Symbol))

Пример 9. Получение данных текущей позиции

Теперь можно проверить торговые условия. В данном примере результаты проверки я буду сохранять в отдельных переменных, а потом эти переменные использовать при принятии окончательного решения: купить или продать. В больших и сложных решениях такой подход себя оправдывает, во-первых, огромной гибкостью, а во-вторых, тем, что итоговый код становится короче. Здесь этот приём использован, в основном, по второй причине: поскольку итоговый алгоритм не совсем нагляден, я пытаюсь повысить наглядность разными способами.

//--- Проверяем торговые условия,
  if( // Условия для BUY
    rates[0].close>maValues[0] // Если первая закрытая свеча закрылась выше МА
    && maValues[0]>maValues[1] // И наклон МА вверх
  )
   {
    // Выставляем флаг торговли
    tradingNeeds = true;
    // и сообщаем советнику направление (здесь - BUY)
    tradingType = POSITION_TYPE_BUY; // чтобы проверить направление уже открытой позиции
    orderType = ORDER_TYPE_BUY;      // чтобы торговать в правильную сторону
    // также посчитаем цену сделки
    requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   }

  else
    if( // условия для SELL
      rates[0].close<maValues[0]
      && maValues[0]<maValues[1]
    )
     {
      tradingNeeds = true;
      tradingType = POSITION_TYPE_SELL;
      orderType = ORDER_TYPE_SELL;
      requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_BID);
     }

Пример 10. Проверка торговых условий

Тот код, который приведён далее, решает, нужно ли торговать прямо сейчас. Выбор момента торговли определяется тремя вопросами:

  • Сложилась ли торговая ситуация? Иначе говоря, закрылась ли свеча за средней? За ответ на этот вопрос отвечает переменная tradingNeeds. Если ответ "нет" (tradingNeeds==false) - торговать не нужно.
  • Есть ли другая открытая позиция? Ответ даст переменная positionExists. Если нет - торгуем смело. Если есть - смотрим последний пункт.
  • Эта позиция сонаправлена или противоположно направлена нашему торговому направлению, обнаруженному в первом пункте? Для ответа нужно сравнить переменные tradingType и positionType. Если они равны, то позиция сонаправлена пришедшему сигналу - и, значит, не торгуем. Противоположно направленную сначала закрываем, а потом создаём новую.

Этот торговый алгоритм можно представить на схеме (рисунок 6).

Схема основных развилок торгового алгоритма

Рисунок 6. Схема основных развилок торгового алгоритма

В MQL5 закрыть позицию и открыть позицию — это действия, предполагающие немедленную сделку. Подход тот же, что вам уже знаком: заполнить структуру торгового приказа и отправить её на сервер. И отличие этих двух структур лишь в том, что при закрытии позиции надо указать её ярлык (ticket) и точно скопировать параметры существующей позиции в запрос. При открытии новой позиции копировать нечего, поэтому мы более свободны, и ярлыка старой позиции нет, поэтому передавать его не нужно.

От предыдущего примера с отложенными приказами код ниже отличается двумя полями:

  • полем action, которые в прошлом случае содержало значение TRADE_ACTION_PENDING, а здесь будет TRADE_ACTION_DEAL,
  • и полем type, которое будет описывать не отложенные, а "прямые" приказы (ORDER_TYPE_SELL или ORDER_TYPE_BUY). 

Для более удобного соотнесения фрагментов кода со схемой на рисунке 6, покрашу код примера в цвета, аналогичные внутренним операторам ветвления на схеме.

У этого кода есть еще два важных отличия от примера 3. Во-первых, перед отправкой торгового приказа его структура проверяется с помощью функции OrderCheck. Это сообщает, если поля структуры заполнены неправильно, причём позволяет получить не только код возврата (retcode), но и его текстовое описание (comment). И, во-вторых, мы пытаемся проконтролировать, удалось ли отправить данные (и принял ли их сервер) или нет. Если происходит ошибка, программа выведет об этом сообщение.

// Если нужно торговать
  if(tradingNeeds)
   {
    // Если существует позиция
    if(positionExists)
     {
      // И она противоположна нужному направлению торговли
      if(positionType != tradingType)
       {
        //--- Позицию нужно закрыть

        //--- Очищаем все участвующие структуры, иначе можно получить ошибку "неверный запрос"
        ZeroMemory(requestClosePosition);
        ZeroMemory(checkResult);
        ZeroMemory(result);
        //--- установка параметров операции
        // Получаем тикет позиции
        requestClosePosition.position = PositionGetInteger(POSITION_TICKET);
        // Закрытие позиции - просто сделка
        requestClosePosition.action = TRADE_ACTION_DEAL;
        // тип позиции противоположен текущему направлению торговли,
        // поэтому для сделки закрытия смело используем текущий тип приказа
        requestClosePosition.type = orderType;
        // Цена текущая
        requestClosePosition.price = requestPrice;
        // Объём операции должен точно соответствовать текущему объёму позиции
        requestClosePosition.volume = PositionGetDouble(POSITION_VOLUME);
        // Установим допустимое максимальное отклонение от текущей цены
        requestClosePosition.deviation = inp_deviation;
        // Символ
        requestClosePosition.symbol = Symbol();
        // Магический номер позиции
        requestClosePosition.magic = EXPERT_MAGIC;


        if(!OrderCheck(requestClosePosition,checkResult))
         {
          // Если ошибочно заполнена структура запроса, выводим сообщение
          PrintFormat("Ошибка при проверке ордера на закрытие позиции: %d - %s",checkResult.retcode, checkResult.comment);
         }
        else
         {
          // Отправляем приказ
          if(!OrderSend(requestClosePosition,result))
           {
            // Если не удалось закрыть позицию, сообщаем
            PrintFormat("Ошибка при закрытии пзиции: %d - %s",result.retcode,result.comment);
           } // if(!OrderSend)
         } // else (!OrderCheck)
       } // if(positionType != tradingType)
      else
       {
        // Позиция открыта в одном направлении с торговым сигналом. Торговать не нужно
        return; 
       } // else(positionType != tradingType)
     } // if(positionExists)

    //--- Открываем новую позицию

    //--- Очищаем все участвующие структуры, иначе можно получить ошибку "неверный запрос"
    ZeroMemory(result);
    ZeroMemory(checkResult);
    ZeroMemory(requestMakePosition);

    // Заполняем структуру запроса
    requestMakePosition.action = TRADE_ACTION_DEAL;
    requestMakePosition.symbol = Symbol();
    requestMakePosition.volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
    requestMakePosition.type = orderType;
    // Пока ждали закрытия позиции, цена могла поменяться
    requestMakePosition.price = orderType == ORDER_TYPE_BUY ?
                                SymbolInfoDouble(_Symbol,SYMBOL_ASK) :
                                SymbolInfoDouble(_Symbol,SYMBOL_BID) ;
    requestMakePosition.sl = orderType == ORDER_TYPE_BUY ?
                             rates[0].low :
                             rates[0].high;
    requestMakePosition.deviation = inp_deviation;
    requestMakePosition.magic = EXPERT_MAGIC;



    if(!OrderCheck(requestMakePosition,checkResult))
     {
      // Если структура не прошла проверку, сообщаем об ошибке проверки
      PrintFormat("Ошибка при проверке ордера новой позиции: %d - %s",checkResult.retcode, checkResult.comment);
     }
    else
     {
      if(!OrderSend(requestMakePosition,result))
       {
        // Если не удалось открыть позицию,сообщаем об ошибке
        PrintFormat("Ошибка при открытии позиции: %d - %s",result.retcode,result.comment);
       } // if(!OrderSend(requestMakePosition

      // Торговля закончена, на всякий случай скидываем флаг
      tradingNeeds = false;
     } // else (!OrderCheck(requestMakePosition))
   } // if(tradingNeeds)

Пример 11. Основной торгующий код (больше всего места занимает заполнение структуры и проверка на ошибки)

Полный текст примера — в прилагаемом файле MADeals.mq5.


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

Классы стандартных индикаторов сложены в папку <Iinclude\Indicators>. Вы можете подключить их все сразу с помощью файла  <Iinclude\Indicators\Indicators.mqh> (обратите внимание на букву 's' в конце имени файла) или по группам ("Trend.mqh", "Oscilators.mqh", "Volumes.mqh", "BillWilliams.mqh"). Есть также возможность подключить отдельно классы для доступа к временным сериям ("TimeSeries.mqh") и класс для работы с пользовательскими индикаторами ("Custom.mqh").

Остальные файлы в этой папке являются вспомогательными и для людей, не владеющих ООП, будут, скорее всего, бесполезны. В каждом "полезном" файле этой папки описано, как правило, несколько классов. Все они именуются схожим образом: первая буква 'C', затем — такое же имя, как у функций создания индикаторов. Например, класс для работы со скользящими средними будет называться CiMA и располагаться в файле "Trend.mqh".

Работа с классами очень похожа на работу с "голыми" функциями MQL5. Отличаются только способ вызова функций да их имена. На первой стадии (создание) нужно вызвать функцию Create и передать ей параметры создаваемого индикатора. На второй стадии (получение данных) нужно вызвать функцию Refresh, обычно без параметров. При необходимости в параметрах этой функции можно указать, какие периоды обновлять, например (OBJ_PERIOD_D1 | OBJ_PERIOD_H1). И на этапе использования применяется функция GetData, чаще всего — с двумя параметрами: номер буфера и индекс свечи (нумерация растёт как в сериях, справа налево).

В примере 12 я привожу минимальный код советника, использующего класс CiMA. Этот советник просто выводит в комментарии значение скользящей средней на первой свече. Если захотите посмотреть, как этот способ работы с индикаторами использовать в торговле, скопируйте советник из предыдущего раздела (MADeals.mq5) в отдельный файл и поменяйте в новом файле соответствующие строки на строки из примера 12.

#include <Indicators\Indicators.mqh>
CiMA g_ma;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
 {
//--- Создаём индикатор
  g_ma.Create(_Symbol,PERIOD_CURRENT,3,0,MODE_SMA,PRICE_CLOSE);

//---
  return(INIT_SUCCEEDED);
 }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
 {
//---
  Comment("");
  
 }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
 {
//--- Получаем данные  
  g_ma.Refresh();
 
//--- Используем
  Comment(
    NormalizeDouble(
      g_ma.GetData(0,1),
      _Digits
    )
  );
 }
//+------------------------------------------------------------------+

Пример 12. Использование класса CiMA (индикатора скользящей средней)


Заключение

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

В следующей статье я хочу рассмотреть советник, технически готовый к публикации на Маркете. В нём будет еще больше проверок, чем во втором советнике этой статьи, но эти проверки позволят советнику стать более надёжным. Немного поменяется структура советника. В нём функция OnTick перестанет быть единственным средоточием бизнес-логики, появятся другие функции. Но главное — появится возможность реагировать на ошибки размещения ордеров (например, из-за реквот). Для этого мы перестроим структуру функции OnTick таким  образом, чтобы к каждой "стадии" работы советника (торговать, ждать свечу, рассчитывать лот...) можно было получить "прямой" доступ, минуя другие стадии. И воспользуемся событием TradeTransaction, чтобы отслеживать ответы сервера. В итоге должен получиться легко редактируемый вами, функционально ориентированный шаблон для построения вашего собственного кода любой сложности (всё ещё без глубокого вникания в ООП, но рабочий).

Список предыдущих статей этого цикла:

Прикрепленные файлы |
TrendPendings.mq5 (13.06 KB)
MADeals.mq5 (21.11 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Utkir Khayrullaev
Utkir Khayrullaev | 7 янв. 2025 в 11:06
Огромное спасибо за ваш труд! Многое стало понятно и легко.
Roman Shiredchenko
Roman Shiredchenko | 19 февр. 2025 в 14:33

прекрасная понятная статья и много чего разъяснено - спасибо вам большое. Особенно в конце как пользоваться можно индикаторами через классы! Круть! рассмотрю к тестам прототипов в своих разработках ТС простых. 

Oleh Fedorov
Oleh Fedorov | 19 мар. 2025 в 11:39
На здоровьечко! Рад, что помогло.
Скользящая средняя на MQL5 с нуля: Просто и доступно Скользящая средняя на MQL5 с нуля: Просто и доступно
На простых примерах разберём принципы расчётов скользящих средних, узнаем о способах оптимизации расчётов индикаторов и, соответственно — скользящих средних.
Нейросети в трейдинге: Повышение эффективности Transformer путем снижения резкости (SAMformer) Нейросети в трейдинге: Повышение эффективности Transformer путем снижения резкости (SAMformer)
Обучение моделей Transformer требует больших объемов данных и часто затруднено из-за слабой способности моделей к обобщению на малых выборках. Фреймворк SAMformer помогает решить эту проблему, избегая плохих локальных минимумов. И повышает эффективность моделей даже на ограниченных обучающих выборках.
Нейросети в трейдинге: Повышение эффективности Transformer путем снижения резкости (Окончание) Нейросети в трейдинге: Повышение эффективности Transformer путем снижения резкости (Окончание)
SAMformer предлагает решение ключевых проблем Transformer в долгосрочном прогнозировании временных рядов, включая сложность обучения и слабое обобщение на малых выборках. Его неглубокая архитектура и оптимизация с учетом резкости обеспечивают избегание плохих локальных минимумов. В данной статье мы продолжим реализацию подходов с использованием MQL5 и оценим их практическую ценность.
Алгоритм атомарного орбитального поиска — Atomic Orbital Search (AOS): Модификация Алгоритм атомарного орбитального поиска — Atomic Orbital Search (AOS): Модификация
Во второй части статьи мы продолжим разработку модифицированной версии алгоритма AOS (Atomic Orbital Search), сфокусировавшись на специфических операторах для повышения его эффективности и адаптивности. После анализа основ и механик алгоритма, мы обсудим идеи по улучшению производительности и возможности анализа сложных пространств решений, предлагая новые подходы для расширения его функциональности как инструмента для оптимизации.