English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Прототип торгового робота

Прототип торгового робота

MetaTrader 5Примеры | 16 августа 2010, 14:14
11 914 10
---
---

Введение

Жизненный цикл любой торговой системы сводится к открытию и закрытию позиции. Это ни у кого не вызывает споров. Но когда дело доходит до реализации алгоритма, то здесь, как говорится, сколько программистов, столько и мнений. Каждый сможет решить одну и ту же задачу своим собственным способом, но с одинаковым окончательным результатом.

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

Этот подход не является на 100% универсальным, но он может поменять ваш метод проектирования логики эксперта. И дело даже не в том, какие возможности работы с ордерами вы хотите использовать в эксперте, вся суть – в том принципе, который закладывается при построении торговой модели.


1. Принципы проектирования торговых систем и виды источников событий

Основным подходом к проектированию алгоритма, которым пользуются большинство, является отслеживание судьбы одной позиции от момента открытия и до закрытия. Это линейный подход. И если требуется внести изменения в код – это часто приводит к большим усложнениям, так как появляется большое число условий и код обрастает новыми ветками анализа.

Лучшим решением для построения модели торгового робота является «обслуживание состояний».  А главный принцип – это анализ не того, как возникло данное состояние эксперта и его позиций и ордеров – а того, что с ними нужно делать сейчас. Этот базовый принцип в корне меняет управление торговлей и упрощает разработку кода.

Рассмотрим его более подробно.

1.1. Принцип «обслуживание состояний»

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

Этот принцип напрямую связан с тем, что эксперт живет от цикла до цикла (в частном случае - от тика до тика), и его не должно волновать, что происходило с ордерами на предыдущем тике. Следовательно, необходимо использовать событийный подход управления ордерами. То есть, на текущем тике эксперт сохраняет свое состояние, которое является исходной точкой для принятия решения на следующем тике.

Например, вам необходимо удалить все отложенные ордера эксперта и только после этого продолжить анализ индикаторов и выставление новых ордеров. Большинство примеров кода, которые приходилось видеть, используют зацикливание типа while (true) { пытаемся удалить } или чуть помягче while (k<1000) { пытаемся удалить; k++; }. Вариант, когда делается однократный вызов команды удаления без анализа ошибок, мы пропустим.

Этот способ линейный, он «вешает» эксперт на неопределенное время.

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

1.2. Второй главный принцип проектирования – это максимально возможное абстрагирование от рассматриваемого направления позиции (покупка/продажа), валюты и графика. Все функции эксперта должны реализовываться так, чтобы явный анализ проверяемого направления или символа осуществлялся в редких случаях, когда этого действительно избежать нельзя (например, когда вы рассматриваете благоприятное  развитие цены для открытой позиции, хотя и здесь возможны варианты ухода от конкретики). Всегда старайтесь избегать такого низкоуровневого проектирования. Это позволит сократить код и сам процесс написания функций минимум в два раза. И сделает их «торгово-независимыми». 

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

1.3. Третий принцип дробление алгоритма на логически лексемы (независимые модули)

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

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

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

Источниками событий для экспертной системы являются:

1. Индикаторы. Примером может служить анализ значений линий индикаторов,  их пересечений, комбинаций и т.д. Также индикаторами могут быть: текущее время, данные получаемые из интернета и т.д. В большинстве случаев индикаторные события служат для сигналов открытия и закрытия ордеров. Реже для их корректировки (обычно трейлинг Stop Loss или отложенного ордера по индикатору).  

Например, практической реализацией работы с индикаторами можно назвать эксперт, анализирующий пересечение быстрой и медленной МА с дальнейшим открытием позиции в сторону пересечения. 

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

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

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

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

Ярким примером таких экспертов являются торгово-информационные системы с графическим управлением торговлей.

Все многообразие экспертов строится на комбинации этих трех источников событий

2. Базовый класс CExpertAdvisor – конструктор экспертов

Что же будет представлять собой работа торгового эксперта? Общая схема взаимосвязей MQL-программы приведена на схеме ниже.

Рисунок 1. Общая схема взаимосвязей элементов программы на MQL5

Рисунок 1. Общая схема взаимосвязей элементов программы на MQL5

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

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

Эту последовательность можно назвать схемой с прямой логикой, так как в ней сначала определяется «ЧТО» будет делать эксперт (какие используются модули обработки событий) и только потом реализуется «КАК» и «ПОЧЕМУ» он будет это делать (получение сигналов событий).

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

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

Приведем пример обратной и прямой логики.  Возьмем открытие/закрытие по сигналу RSI.

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

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

А в первом варианте придётся пересматривать структуру обработки сигнала или вставлять его отдельной функцией.

Рекомендация: при описании торговой системы вы должны начинать не со слов "1. Получаем сигнал... открываем ордер", а сразу разбивать на разделы: "а) Условие открытия ордеров, б) Условия сопровождения ордеров и т.д." и в каждом уже анализировать требуемые сигналы.

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

Рисунок 2. Примеры реализации экспертов

Рисунок 2. Примеры реализации экспертов

а). Эксперт, основанный только на сигналах какого-либо индикатора. Может открывать и закрывать позиции при смене сигнала. Пример - эксперт по МА.
б). Эксперт с графическим управлением торговли.
в). Эксперт на основе индикаторов, но уже с добавлением трейлинга Stop Loss и временем работы. Пример - скальпинг на новостях с открытием позиции в сторону тренда по индикатору МА.
г).  Безиндикаторный эксперт, с усреднением позиции, проверяющий параметры позиции только один раз на открытии нового бара. Пример - усредняющий эксперт.

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


3. Реализация класса эксперта

Создадим класс по всем выше описанным правилам и требованиям, который будет являться основой всех будущих экспертов.

Минимум функциональности, которая должна быть в классе CExpertAdvisor, выглядит следующим образом:

1. Инициализация:

  • Регистрация индикаторов;
  • Установка начальных значений параметров;
  • Настройка на требуемый символ и таймфрейм.

2. Функции получения сигналов

  • Разрешенное время работы (торгуемые интервалы);
  • Определение сигнала для открытия/закрытия позиций или ордеров;
  • Определение фильтра (тренда, времени и т.д.).  
  • Запуск, остановка таймера.

3. Сервисные функции

  • Вычисление цены открытия, уровни SL и TP, объем ордера;
  • Отправка торговых запросов (открытие, закрытие, модификация).

4. Торговые модули

  • Обработка сигналов, фильтров;
  • Контроль позиций и ордеров;
  • Работа в функциях эксперта: OnTrade(), OnTimer(), OnTester(), OnChartEvent().

5. Деинициализация

  • Вывод сообщений, отчетов;
  • Чистка графика, выгрузка индикаторов.

Все функции класса разбиваются на три группы. Общая схема вложенности функций и их описание представлены ниже.

Рисунок 3. Схема вложенности функций эксперта

Рисунок 3. Схема вложенности функций эксперта

1. Функции-макросы 

Эта небольшая группа функций является базой для работы с типами ордеров, параметрами символа и значениями цен для установки ордеров (открытие и стопы). Эти макросы полностью обеспечивают второй принцип проектирования – абстрактность. Они работают в контексте того символа, на котором работает сам эксперт.

Макросы конвертации типов работают с понятием направления рынка – покупка или продажа. Поэтому, чтобы не вводить свои константы, лучше использовать имеющиеся – ORDER_TYPE_BUY и ORDER_TYPE_SELL.  Ниже представлены несколько примеров использования макросов и результаты их работы.

   //--- Макросы конвертации типов
   long       BaseType(long dir);        // возвращает базовый тип ордера для указанного направления
   long       ReversType(long dir);      // возвращает обратный тип ордера для указанного направления
   long       StopType(long dir);        // возвращает тип стоп-ордера для указанного направления
   long       LimitType(long dir);       // возвращает тип лимит-ордера для указанного направления

   //--- Макросы нормализации
   double     BasePrice(long dir);       // возвращает цену Bid/Ask для указанного направления
   double     ReversPrice(long dir);     // возвращает цену Bid/Ask для обратного направления

   long dir,newdir;
   dir=ORDER_TYPE_BUY;
   newdir=ReversType(dir);               // newdir=ORDER_TYPE_SELL
   newdir=StopType(dir);                 // newdir=ORDER_TYPE_BUY_STOP
   newdir=LimitType(dir);                // newdir=ORDER_TYPE_BUY_LIMIT
   newdir=BaseType(newdir);              // newdir=ORDER_TYPE_BUY

   double price;
   price=BasePrice(dir);                 // price=Ask
   price=ReversPrice(dir);               // price=Bid

При разработке экспертов макросы позволяют не конкретизировать обрабатываемое направление и помогают создавать короткий код.

2. Сервисные функции

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

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

Примеры исполнительных функций:  закрыть указанные ордера; изменить Stop Loss в указанной позиции и т.д.

Эта группа является самой большой. Это и есть тот функционал, на котором базируется вся рутинная работа эксперта. В большом количестве примеры этих функций можно найти на форуме по адресу https://www.mql5.com/ru/forum/107476. Но кроме этого в стандартной библиотеке MQL5 уже написаны классы, которые берут на себя часть работы по управлению ордерами и позициями, а именно – класс CTrade.

Но любая ваша задача будет требовать создания новых реализаций или небольшую модификацию существующих.

3. Модули обработки событий

Группа этих функций является уже высокоуровневой надстройкой над первыми двумя группами. Как уже говорилось выше – это есть те готовые блоки, из которых собирается ваш эксперт. В общем-то, именно они попадают в функции-обработчики событий MQL-программы OnStart(), OnTick(), OnTimer(), OnTrade(), OnChartEvent(). Эта группа немногочисленна, и содержимое данных модулей может корректироваться от задачи к задаче. Но принципиально ничего не меняется.  

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

Итак, приступим к реализации

1. Инициализация, деинициализация

class CExpertAdvisor
  {
protected:
   bool              m_bInit;       // флаг корректной инициализации
   ulong             m_magic;       // магик эксперта
   string              m_smb;       // символ, на котором работает эксперт
   ENUM_TIMEFRAMES      m_tf;       // рабочий таймфрейм
   CSymbolInfo      m_smbinf;       // параметры символа
   int               m_timer;       // время для таймера

public:
   double              m_pnt;       // учет для стопов 5/3 знаковых котировок
   CTrade            m_trade;       // объект для исполнения торговых приказов
   string              m_inf;       // строка комментария для информации о работе эксперта

Это минимальный требуемый набор параметров для работы функций эксперта.

Два параметра m_smb и m_tf специально вынесены в свойства эксперта для того, чтобы без особого труда указывать эксперту, на какой валюте и на каком периоде работать. Например, если присвоить m_smb="USDJPY", то эксперт будет работать на этом символе, независимо от того, на каком он запущен.  Если присвоить tf=PERIOD_H1, то все сигналы и анализ индикаторов будут происходить на часовом графике. 

Далее идут методы класса. Первые три метода относятся к инициализации и деинициализации эксперта.

public:
   //--- Инициализация
   void              CExpertAdvisor();                               // конструктор
   void             ~CExpertAdvisor();                               // деструктор
   virtual bool      Init(long magic,string smb,ENUM_TIMEFRAMES tf); // инициализация

Конструктор и деструктор в базовом классе ничем не занимаются.

Метод Init() проводит начальную инициализацию параметров эксперта по символу, таймфрейму и магику.

//------------------------------------------------------------------ CExpertAdvisor
void CExpertAdvisor::CExpertAdvisor()
  {
   m_bInit=false;
  }
//------------------------------------------------------------------ ~CExpertAdvisor
void CExpertAdvisor::~CExpertAdvisor()
  {
  }
//------------------------------------------------------------------ Init
bool CExpertAdvisor::Init(long magic,string smb,ENUM_TIMEFRAMES tf)
  {
   m_magic=magic; m_smb=smb; m_tf=tf;         // задание инициирующих параметров
   m_smbinf.Name(m_smb);                      // инициализация символа
   m_pnt=m_smbinf.Point();                    // вычисление множителя для 5/3 котировки
   if(m_smbinf.Digits()==5 || m_smbinf.Digits()==3) m_pnt*=10;  
   m_trade.SetExpertMagicNumber(m_magic);     // установка магика для эксперта

   m_bInit=true; return(true);                // "торговля разрешена"
  }

2. Функции получения сигналов

Эти функции занимаются анализом рынка и индикаторов.

   bool              CheckNewBar();                          // проверка появления нового бара
   bool              CheckTime(datetime start,datetime end); // проверка разрешенного времени работы
   virtual long      CheckSignal(bool bEntry);               // проверка сигнала 
   virtual bool      CheckFilter(long dir);                  // проверка фильтра для направления

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

//------------------------------------------------------------------ CheckNewBar
bool CExpertAdvisor::CheckNewBar()          // функция проверки появления нового бара
  {
   MqlRates rt[2];
   if(CopyRates(m_smb,m_tf,0,2,rt)!=2)      // копируем бары
     { Print("CopyRates of ",m_smb," failed, no history"); return(false); }
   if(rt[1].tick_volume>1) return(false);   // проверяем объем 
   return(true);
  }
//---------------------------------------------------------------   CheckTime
bool CExpertAdvisor::CheckTime(datetime start,datetime end)
  {
   datetime dt=TimeCurrent();                          // текущее время
   if(start<end) if(dt>=start && dt<end) return(true); // проверяем нахождение в промежутке
   if(start>=end) if(dt>=start|| dt<end) return(true);
   return(false);
  }

Вторые две всегда зависят от тех индикаторов, которые вы используете. Задать эти функции на все случаи просто невозможно.

Главное – важно понимать, что сигнальные функции CheckSignal() и CheckFilter() могут анализировать абсолютно любые индикаторы и их комбинации! То есть, торговые модули, в которые впоследствии попадут эти сигналы, являются независимыми от самих источников.

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

3. Сервисные функции

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

   double         CountLotByRisk(int dist,double risk,double lot); // вычисление лота по размеру риска
   ulong          DealOpen(long dir,double lot,int SL,int TP);     // совершение сделки с указанными параметрами
   ulong          GetDealByOrder(ulong order);                     // получить тикет сделки по тикету ордера
   double         CountProfitByDeal(ulong ticket);                 // посчитать полученную прибыль по тикету сделки
//------------------------------------------------------------------ CountLotByRisk
double CExpertAdvisor::CountLotByRisk(int dist,double risk,double lot) // вычисление лота по размеру риска
  {
   if(dist==0 || risk==0) return(lot);
   m_smbinf.Refresh();
   return(NormalLot(AccountInfoDouble(ACCOUNT_BALANCE)*risk/(dist*10*m_smbinf.TickValue())));
  }
//------------------------------------------------------------------ DealOpen
ulong CExpertAdvisor::DealOpen(long dir,double lot,int SL,int TP)
  {
   double op,sl,tp,apr,StopLvl;
   // определили параметры цены
   m_smbinf.RefreshRates(); m_smbinf.Refresh();
   StopLvl = m_smbinf.StopsLevel()*m_smbinf.Point(); // запомнили стоп уровень
   apr     = ReversPrice(dir); 
   op      = BasePrice(dir);                         // цена открытия
   sl      = NormalSL(dir, op, apr, SL, StopLvl);    // stop loss
   tp      = NormalTP(dir, op, apr, TP, StopLvl);    // take profit

   // открываем позицию
   m_trade.PositionOpen(m_smb,(ENUM_ORDER_TYPE)dir,lot,op,sl,tp);
   ulong order = m_trade.ResultOrder(); 
   if(order<=0) return(0);                           // тикет ордера
   return(GetDealByOrder(order));                    // вернули тикет сделки
  }
//------------------------------------------------------------------ GetDealByOrder
ulong CExpertAdvisor::GetDealByOrder(ulong order) // получение тикета сделки по тикету ордера
  {
   PositionSelect(m_smb);
   HistorySelectByPosition(PositionGetInteger(POSITION_IDENTIFIER));
   uint total=HistoryDealsTotal();
   for(uint i=0; i<total; i++)
     {
      ulong deal=HistoryDealGetTicket(i);
      if(order==HistoryDealGetInteger(deal,DEAL_ORDER))
         return(deal);                            // запомнили тикет сделки 
     }
   return(0);
  }
//------------------------------------------------------------------ CountProfit
double CExpertAdvisor::CountProfitByDeal(ulong ticket)  // профит позиции по тикету сделки
  {
   CDealInfo deal; deal.Ticket(ticket);                 // тикет сделки
   HistorySelect(deal.Time(),TimeCurrent());            // выбрать все сделки после данной
   uint total  = HistoryDealsTotal();
   long pos_id = deal.PositionId();                     // получаем идентификатор позиции
   double prof = 0;
   for(uint i=0; i<total; i++)                          // ищем все сделки с этим идентификатором
     {
      ticket = HistoryDealGetTicket(i);
         if(HistoryDealGetInteger(ticket,DEAL_POSITION_ID)!=pos_id) continue;
      prof += HistoryDealGetDouble(ticket,DEAL_PROFIT); // суммируем профит
     }
   return(prof);                                        // возвращаем профит
  }

4. Торговые модули

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

   virtual bool      Main();                            // главный модуль контроля торгового процесса
   virtual void      OpenPosition(long dir);            // модуль открытия позиций
   virtual void      CheckPosition(long dir);           // проверка позиции и открытие дополнительных
   virtual void      ClosePosition(long dir);           // закрытие позиции
   virtual void      BEPosition(long dir,int BE);       // перемещение stop loss в безубыток
   virtual void      TrailingPosition(long dir,int TS); // трейлинг stop loss
   virtual void      OpenPending(long dir);             // модуль открытия отложенных ордеров
   virtual void      CheckPending(long dir);            // работа с текущими ордерами и открытие дополнительных
   virtual void      TrailingPending(long dir);         // перемещение отложенных ордеров
   virtual void      DeletePending(long dir);           // удаление отложенных ордеров

Более конкретные реализации этих функций рассмотрим в примерах ниже.

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

Конечно же, ваши эксперты не ограничиваются только ими. В классе CExpertAdvisor объявлены только самые необходимые методы. Вы можете добавлять новые обработчики в классах-потомках, менять существующие, расширять свои модули, создавая свою единую библиотеку. При наличии такой библиотеки разработка эксперта «под ключ» занимает по времени от получаса до двух дней.


4. Примеры использования класса CExpertAdvisor

4.1. Пример работы на основе сигналов индикатора

В качестве первого примера начнем, пожалуй, с самой простой задачи - рассмотрим советник MovingAverage (базовый пример из MetaTrader 5) с использованием класса CExpertAdvisor, только немного усложним его по функциональности.

Алгоритм:

а) Условие открытия позиции

  • Если цена пересекает МА снизу вверх, то открываем позицию на покупку;
  • Если цена пересекает МА сверху вниз, то открываем позицию на продажу;
  • Устанавливаем Stop Loss SL, TakeProfit TP;
  • Лот позиции высчитывается по параметру Risk – сколько потеряем от депозита при срабатывании Stop Loss.

б) Условие закрытия позиции

  • Если цена пересекает МА снизу вверх, то закрываем позицию на продажу;
  • Если цена пересекает МА сверху вниз, то закрываем позицию на покупку.

в) Ограничение работы

  • Ограничиваем работу эксперта по времени с HourStart до HourEnd ежедневно;
  • Эксперт проводит торговые операции только при появлении нового бара.

г) Сопровождение позиции

  • Используем простой трейлинг-стоп на расстоянии TS.

Для работы эксперта понадобится семь функций из класса CExpertAdvisor:

  • Сигнальная функция CheckSignal();  
  • Фильтр тиков CheckNewBar();
  • Фильтр времени CheckTime();
  • Сервисная функция открытия позиций DealOpen();
  • Три рабочих модуля OpenPosition(), ClosePosition(), TrailingPosition().

Функцию CheckSignal() и модули надо определить в классе потомке для решения конкретно его задачи. Также необходимо добавить инициализацию индикатора.

//+------------------------------------------------------------------+
//|                                              Moving Averages.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "ExpertAdvisor.mqh"

input double Risk      = 0.1; // Риск
input int    SL        = 100; // Расстояние Stop Loss
input int    TP        = 100; // Расстояние Take Profit
input int    TS        =  30; // Расстояние трейлинг стопа
input int    pMA       =  12; // Период скользящей средней
input int    HourStart =   7; // Час начала торговли
input int    HourEnd   =  20// Час окончания торговли
//---
class CMyEA : public CExpertAdvisor
  {
protected:
   double            m_risk;          // размер риска
   int               m_sl;            // Stop Loss
   int               m_tp;            // Take Profit
   int               m_ts;            // трейлинг
   int               m_pMA;           // MA период
   int               m_hourStart;     // час начала торговли
   int               m_hourEnd;       // час окончания торговли
   int               m_hma;           // индикатор МА
public:
   void              CMyEA();
   void             ~CMyEA();
   virtual bool      Init(string smb,ENUM_TIMEFRAMES tf); // инициализация
   virtual bool      Main();                              // главная функция
   virtual void      OpenPosition(long dir);              // открыть позицию по сигналу
   virtual void      ClosePosition(long dir);             // закрыть позицию по сигналу
   virtual long      CheckSignal(bool bEntry);            // проверить сигнал
  };
//------------------------------------------------------------------ CMyEA
void CMyEA::CMyEA() { }
//----------------------------------------------------------------- ~CMyEA
void CMyEA::~CMyEA()
  {
   IndicatorRelease(m_hma); // удаляем индикатор МА
  }
//------------------------------------------------------------------ Init
bool CMyEA::Init(string smb,ENUM_TIMEFRAMES tf)
  {
   if(!CExpertAdvisor::Init(0,smb,tf)) return(false);  // инициализация родительского класса

   m_risk=Risk; m_tp=TP; m_sl=SL; m_ts=TS; m_pMA=pMA;  // копируем параметры
   m_hourStart=HourStart; m_hourEnd=HourEnd;

   m_hma=iMA(m_smb,m_tf,m_pMA,0,MODE_SMA,PRICE_CLOSE); // создаем индикатор МА
   if(m_hma==INVALID_HANDLE) return(false);            // если ошибка то выходим
   m_bInit=true; return(true);                         // "торговля разрешена"
  }
//------------------------------------------------------------------ Main
bool CMyEA::Main()                            // главная функция
  {
   if(!CExpertAdvisor::Main()) return(false); // вызов функции родительского класса

   if(Bars(m_smb,m_tf)<=m_pMA) return(false); // если мало баров
   
   if(!CheckNewBar()) return(true);           // проверяем новый бар

   // проверяем каждое направление
   long dir;
   dir=ORDER_TYPE_BUY;
   OpenPosition(dir); ClosePosition(dir); TrailingPosition(dir,m_ts);
   dir=ORDER_TYPE_SELL;
   OpenPosition(dir); ClosePosition(dir); TrailingPosition(dir,m_ts);

   return(true);
  }
//------------------------------------------------------------------ OpenPos
void CMyEA::OpenPosition(long dir)
  {
   if(PositionSelect(m_smb)) return;     // если ордер есть, то выходим
   if(!CheckTime(StringToTime(IntegerToString(m_hourStart)+":00"),
                 StringToTime(IntegerToString(m_hourEnd)+":00"))) return;
   if(dir!=CheckSignal(true)) return;    // если сигнала на текущее направление нет
   double lot=CountLotByRisk(m_sl,m_risk,0);
   if(lot<=0) return;                    // если лот не определен, выходим
   DealOpen(dir,lot,m_sl,m_tp);          // открываем позицию
  }
//------------------------------------------------------------------ ClosePos
void CMyEA::ClosePosition(long dir)
  {
   if(!PositionSelect(m_smb)) return;                 // если позиции нет, то выходим
   if(!CheckTime(StringToTime(IntegerToString(m_hourStart)+":00"),
                 StringToTime(IntegerToString(m_hourEnd)+":00")))
     { m_trade.PositionClose(m_smb); return; }        // если не время торговли, то закрываем ордера
   if(dir!=PositionGetInteger(POSITION_TYPE)) return; // если позиция не проверяемого направления
   if(dir!=CheckSignal(false)) return;                // если сигнал закрытия не совпал с текущей позицией
   m_trade.PositionClose(m_smb,1);                    // закрываем позицию
  }
//------------------------------------------------------------------ CheckSignal
long CMyEA::CheckSignal(bool bEntry)
  {
   MqlRates rt[2];
   if(CopyRates(m_smb,m_tf,0,2,rt)!=2)
     { Print("CopyRates ",m_smb," не загружена история"); return(WRONG_VALUE); }

   double ma[1];
   if(CopyBuffer(m_hma,0,0,1,ma)!=1)
     { Print("CopyBuffer MA - нет данных"); return(WRONG_VALUE); }

   if(rt[0].open<ma[0] && rt[0].close>ma[0])
      return(bEntry ? ORDER_TYPE_BUY:ORDER_TYPE_SELL); // условие для покупок
   if(rt[0].open>ma[0] && rt[0].close<ma[0])
      return(bEntry ? ORDER_TYPE_SELL:ORDER_TYPE_BUY); // условие для продаж

   return(WRONG_VALUE);                                // если сигнала нет
  }

CMyEA ea; // экземпляр класса
//------------------------------------------------------------------ OnInit
int OnInit()
  {
   ea.Init(Symbol(),Period()); // инициализируем эксперт
   return(0);
  }
//------------------------------------------------------------------ OnDeinit
void OnDeinit(const int reason) { }
//------------------------------------------------------------------ OnTick
void OnTick()
  {
   ea.Main();                  // обрабатываем пришедший тик
  }

Разберем по порядку структуру функции Main(). Условно она делится на две части.

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

Во второй части выполняется непосредственная обработка событий рынка.

Проверяется фильтр CheckNewBar()  – проверка появления нового бара. И по порядку вызываются модули для двух направлений торговли.

В модулях организовано все довольно абстрактно (выполняем второй принцип проектирования). Прямое обращение к свойству символа не происходит. И три модуля OpenPosition(), ClosePosition() и TrailingPosition() оперируют только теми параметрами, которые приходят к ним извне. Это и позволяет вызывать данные модули как для проверки ордеров на покупку, так и на продажу. 


4.2. Пример использования CExpertAdvisor - безиндикаторный эксперт с анализом состояния и результата позиций

Для демонстрации возьмём систему, торгующую только на переворот позиции с увеличением лота после убытка (такой тип экспертов обычно называется мартингейлом)

а) Открытие начального ордера

  •  при старте эксперт открывает первую позицию на покупку с начальным лотом

б) Открытия последующих позиций

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

Для работы эксперта понадобится три функций из базового класса CExpertAdvisor:

  • открытие позиции DealOpen().
  • получение значения профита закрытой позиции по тикету сделки CountProfitByDeal()
  • рабочие модули OpenPosition(), CheckPosition()

Так как эксперт не анализирует никакие индикаторы, а только результаты сделок, то оптимальным для производительности будет использование события OnTrade(). То есть эксперт, установив первый инициирующий ордер на покупку, все последующие ордера будет устанавливать только после закрытия этой позиции. Поэтому открытие начального ордера выполним в обработчике OnTick(), а последующую работу в обработчике OnTrade() эксперта.

Функция Init(), как обычно, просто инициализирует параметры класса внешними параметрами эксперта.

Модуль OpenPosition() открывает инициирующую позицию и блокируется по флагу m_first.

Модуль CheckPosition() контролирует дальнейшие перевороты позиции.

Эти модули вызываются в соответствующих функциях эксперта: OnTick() и OnTrade().

//+------------------------------------------------------------------+
//|                                                       eMarti.mq5 |
//|              Copyright Copyright 2010, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "ExpertAdvisor.mqh"
#include <Trade\DealInfo.mqh>

input double Lots    = 0.1; // Лот
input double LotKoef = 2;   // множитель для лота при убытке
input int    Dist    = 60;  // расстояние до Stop Loss и Take Profit 
//---
class CMartiEA : public CExpertAdvisor
  {
protected:
   double            m_lots;       // Лот
   double            m_lotkoef;    // множитель для лота при убытке
   int               m_dist;       // расстояние до Stop Loss и Take Profit
   CDealInfo         m_deal;       // последняя сделка
   bool              m_first;      // флаг открытия первой позиции
public:
   void              CMartiEA() { }
   void             ~CMartiEA() { }
   virtual bool      Init(string smb,ENUM_TIMEFRAMES tf); // инициализация
   virtual void      OpenPosition();
   virtual void      CheckPosition();
  };
//------------------------------------------------------------------ Init
bool CMartiEA::Init(string smb,ENUM_TIMEFRAMES tf)
  {
   if(!CExpertAdvisor::Init(0,smb,tf)) return(false); // инициализация родительского класса
   m_lots=Lots; m_lotkoef=LotKoef; m_dist=Dist;       // скопировали параметры
   m_deal.Ticket(0); m_first=true;
   m_bInit=true; return(true);                        // "торговля разрешена"
  }
//------------------------------------------------------------------ OnTrade
void CMartiEA::OpenPosition()
  {
   if(!CExpertAdvisor::Main()) return;                       // вызов родительской функции
   if(!m_first) return;                                      // если уже открывали начальную позицию
   ulong deal=DealOpen(ORDER_TYPE_BUY,m_lots,m_dist,m_dist); // открыли начальную позицию
   if(deal>0) { m_deal.Ticket(deal); m_first=false; }        // если позиция существует
  }
//------------------------------------------------------------------ OnTrade
void CMartiEA::CheckPosition()
  {
   if(!CExpertAdvisor::Main()) return;           // вызов родительской функции
   if(m_first) return;                           // если еще не открыли начальную позицию
   if(PositionSelect(m_smb)) return;             // если позиция существует

   // проверяем профит предыдущей позиции
   double lot=m_lots;                            // начальный лот
   long dir=m_deal.Type();                       // предыдущее направление
   if(CountProfitByDeal(m_deal.Ticket())<0)      // если был убыток
     {
      lot=NormalLot(m_lotkoef*m_deal.Volume());  // увеличили лот
      dir=ReversType(m_deal.Type());             // перевернули позицию
     }
   ulong deal=DealOpen(dir,lot,m_dist,m_dist);   // открыли позицию
   if(deal>0) m_deal.Ticket(deal);               // запомнили тикет
  }

CMartiEA ea; // экземпляр объекта
//------------------------------------------------------------------ OnInit
int OnInit()
  {
   ea.Init(Symbol(),Period()); // инициализируем эксперт
   return(0);
  }
//------------------------------------------------------------------ OnDeinit
void OnDeinit(const int reason) { }
//------------------------------------------------------------------ OnTick
void OnTick()
  {
   ea.OpenPosition();          // обрабатываем тик - открываем первый ордер
  }
//------------------------------------------------------------------ OnTrade
void OnTrade()
  {
   ea.CheckPosition();         // обрабатываем торговое событие
  }


5. Работа с событиями

В статье вы познакомились с примерами обработкой двух событий – NewTick и Trade, представленными функциями OnTick() и OnTrade() соответственно. В большинстве случаев, именно эти два события являются постоянно используемыми.

Для экспертов существуют еще четыре функции обработки событий:

  • OnChartEvent является обработчиком большой группы событий: при работе с графическими объектами, клавиатурой, мышкой и пользовательскими событиями. Например, функцию используют при создании интерактивных экспертов или экспертов, построенных на принципе графического управления ордерами. Или же просто для создания активных элементов управления параметрами MQL-программы (используя кнопки и поля ввода). В общем, эта функция применяется для обработки внешнего события эксперта.
  • OnTimer вызывается при обработке события системного таймера. Она используется для случаев, когда MQL-программе требуется выполнять с постоянной периодичностью анализ своего окружения, расчеты значений индикаторов, или когда необходимо постоянно обращаться во внешние источники сигналов и т.д. Грубо говоря, функция OnTimer() – это альтернативная, можно даже сказать, лучшая замена конструкции типа:
    while(true) {  /* выполнить анализ */; Sleep(1000); }. 
    То есть эксперту не надо работать в бесконечном цикле при своем старте, а достаточно перенести вызовы своих функций из функции OnTick() в OnTimer().
  • OnBookEvent является обработчиком  события, которое генерируется при изменении состояния стакана цен. Данное событие можно отнести к внешним и выполнять его обработку в соответствии с поставленной задачей.
  • OnTester вызывается по окончании тестирования эксперта на заданном интервале дат перед функцией OnDeinit() для возможности отсеивания поколений тестирования при генетической оптимизации по параметру Custom max.

Не стоит забывать, что любые события и их комбинации всегда целесообразно использовать для решения своей определенной задачи


Послесловие

Как видите, написание эксперта при наличии верно составленной схемы не отнимает много времени. Благодаря тому, что в MQL5 появились новые возможности обработки событий, расширяется структура управления торговым процессом. Но все это богатство становится действительно мощным инструментом в работе только в том случае, если вы будете правильно составлять свои торговые алгоритмы.

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

Прикрепленные файлы |
expertadvisor.mqh (18.14 KB)
emyea.mq5 (5.95 KB)
emarti.mq5 (3.66 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (10)
[Удален] | 18 авг. 2010 в 05:23
sergeev:
а чем работа по таймеру отличается от работы по по тикам?

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

Таймер нужен для выполнения периодических действий, еще там желательно выполнять часть кода мультивалютника (второе рекомендуется, но не обязательно).

PS

Таймер также интересно использовать для отслеживания состояния терминала. К примеру наличие коннекта...

Igor Volodin
Igor Volodin | 29 авг. 2010 в 13:49
Процедурно вышло, но для начала неплохо. Можно многое скрыть используя ООП. С минимально видимым интерфейсом в каждом новом советнике. Вынести стратегию в отдельный класс, к примеру. Настройки стратегии - свойства объекта CStrategy, инициировать в конструкторе эксперта. Мультивалютный - инициировать список объектов стратегий. Каждая стратегия на своем символе. Ну и мелочи всякие, вместо m_trade.ResultOrder() можно сразу использовать m_trade.ResultDeal() без лишних циклов по всем ордерам в дополнительной функции GetDealByOrder().
Alexey Klenov
Alexey Klenov | 30 авг. 2010 в 13:10

Рекомендую уйти вот от такой конструкции

//------------------------------------------------------------------ CheckNewBar
bool CExpertAdvisor::CheckNewBar()          // функция проверки появления нового бара
  {
   MqlRates rt[2];
   if(CopyRates(m_smb,m_tf,0,2,rt)!=2)      // копируем бары
     { Print("CopyRates of ",m_smb," failed, no history"); return(false); }
   if(rt[1].tick_volume>1) return(false);   // проверяем объем 
   return(true);
  }

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

соответственно возможно пропустить открытие.

Лучше привязываться к времени открытия бара, но для этого нужно где то сохранять предыдущее время  к примеру нулевого бара чтобы потом сравнивать его с текущем временем нулевого бара

Если одинаковое - нового бара нет

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

Такая конструкция более надежная.

Дмитрий Александрович
Дмитрий Александрович | 4 сент. 2010 в 12:05
olyakish:

Рекомендую уйти вот от такой конструкции

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

соответственно возможно пропустить открытие.

Лучше привязываться к времени открытия бара, но для этого нужно где то сохранять предыдущее время  к примеру нулевого бара чтобы потом сравнивать его с текущем временем нулевого бара

Если одинаковое - нового бара нет

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

Такая конструкция более надежная.

Я у себя так делал:

bool CUniexp::checkNewBar(void)
{
   static datetime prevTime[1];
   datetime currentTime[0];
   CopyTime(_Symbol,_Period,0,1,currentTime);
   if (currentTime[0]==prevTime[0])
   {return (false);}
   else
   {
      prevTime[0] = currentTime[0];
      return (true);
   }
}


Prival
Prival | 4 сент. 2010 в 12:21
20 торговых сигналов на MQL5 20 торговых сигналов на MQL5
В этой статье вы научитесь получать торговые сигналы, необходимые для работы торговой системы. Приведены примеры формирования 20 торговых сигналов в виде отдельных пользовательских функций, которые можно использовать в написании экспертов. Для вашего удобства все функции из статьи собраны в один включаемый mqh-файл, который легко подключается к будущему эксперту.
Применение функции TesterWithdrawal() для моделирования снятия прибыли Применение функции TesterWithdrawal() для моделирования снятия прибыли
В статье рассмотрено применение функции TesterWithDrawal() для оценки рисков в торговых системах, выполняющих снятие определенной части средств в процессе работы. Наряду с этим показано, как применение данной функции влияет на алгоритм расчета просадки по средствам в тестере. Использование данной функции может быть полезным при оптимизации параметров вашего советника.
Интервью с Леонидом Величковским: &quot;Главный миф о нейронных сетях – сверхприбыльность&quot; (ATC 2010) Интервью с Леонидом Величковским: &quot;Главный миф о нейронных сетях – сверхприбыльность&quot; (ATC 2010)
Герой нашего интервью - Леонид Величковский (LeoV) – уже принимал участие в Чемпионатах по автоматическому трейдингу. В 2008 году его мультивалютная нейронная сеть ярко вспыхнула на небосклоне, заработав в определенный момент 110 000 $, но в итоге пала жертвой собственного агрессивного мани-менеджмента. В интервью двухлетней давности Леонид говорил о собственном опыте трейдинга и особенностях работы его советника. В преддверии же Чемпионата ATC 2010 наш герой рассказывает о самых распространенных мифах и заблуждениях, связанных с нейросетями.
Несколько способов определения тренда на MQL5 Несколько способов определения тренда на MQL5
Любой трейдер отдал бы многое за возможность безошибочного определения тренда в любой момент времени, и, пожалуй, это и есть тот самый Грааль, который все ищут. В данной статье мы рассмотрим несколько способов определения тренда, а точнее, как средствами языка MQL5 запрограммировать несколько классических способов определения тренда.