Прототип торгового робота
Введение
Жизненный цикл любой торговой системы сводится к открытию и закрытию позиции. Это ни у кого не вызывает споров. Но когда дело доходит до реализации алгоритма, то здесь, как говорится, сколько программистов, столько и мнений. Каждый сможет решить одну и ту же задачу своим собственным способом, но с одинаковым окончательным результатом.
За годы практики программирования было перепробовано несколько подходов к построению логики и структуры экспертов. На данный момент можно утверждать, что создана четкая схема шаблона, который применяется во всех кодах.
Этот подход не является на 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
Как видно из схемы, на первом плане стоит вход в рабочий цикл (это может быть тик или сигнал таймера). На этом этапе в первом блоке есть возможность отфильтровать этот тик без его обработки. Это делается в тех случаях, когда работа эксперта не нужна на каждом тике, а только при появлении нового бара или если эксперту просто запрещено работать.
Затем выполнение программы переходит во второй блок - модули работы с ордерами и позициями, и только потом из модулей вызываются блоки обработки событий. Каждый модуль может опрашивать только своё интересуемое событие.
Эту последовательность можно назвать схемой с прямой логикой, так как в ней сначала определяется «ЧТО» будет делать эксперт (какие используются модули обработки событий) и только потом реализуется «КАК» и «ПОЧЕМУ» он будет это делать (получение сигналов событий).
Прямая логика вполне соответствует нашему мировосприятию и общечеловеческой логике. Ведь человек мыслит сначала конкретными понятиями, затем их обобщает и только потом систематизирует и выделяет взаимосвязи.
Проектирование экспертов в этом плане не исключение. Сначала объявляется, что должен делать эксперт (открывать и закрывать позиции, подтягивать защитный стоп), и только потом детализируется, при каких событиях и как он должен это делать. Но ни в коем случает не наоборот: получили сигнал, и думаем, куда его прописать и чем обработать. Это обратная логика, и ею лучше не пользоваться, так как в результате вы получите громоздкий код с большим числом веток условий.
Приведем пример обратной и прямой логики. Возьмем открытие/закрытие по сигналу RSI.
- В обратной логике эксперт начинает свою работу с получения значения индикатора, а затем проверяет направление сигнала и что нужно сделать с позицией: открыть покупку и закрыть продажу, или наоборот - открыть продажу и закрыть покупку. То есть, точкой входа служит получение и анализ сигнала.
- В прямой логике все происходит наоборот. Эксперт имеет два модуля: открытия и закрытия позиции и он просто проверяет условия для выполнения этих модулей. То есть, войдя в модуль открытия, эксперт получает значение индикатора и проверяет, является ли он сигналом на открытие. Потом, войдя в модуль закрытия ордера, эксперт проверяет, является ли он сигналом на закрытие позиции. То есть, точки входа как таковой нет - есть независимо работающие модули анализа состояния системы (первый принцип проектирования).
Теперь, если вы захотите усложнить эксперт, то во втором варианте вам это будет сделать намного проще, чем в первом. Достаточно будет создать новый модуль обработки событий.
А в первом варианте придётся пересматривать структуру обработки сигнала или вставлять его отдельной функцией.
Для лучшего понимания этого подхода, приведем различные схемы работы в контекстах четырех различных экспертов.
Рисунок 2. Примеры реализации экспертов
а). Эксперт, основанный только на сигналах какого-либо индикатора. Может открывать и закрывать позиции при смене сигнала. Пример - эксперт по МА.
б). Эксперт с графическим управлением торговли.
в). Эксперт на основе индикаторов, но уже с добавлением трейлинга Stop Loss и временем работы. Пример - скальпинг на новостях с открытием позиции в сторону тренда по индикатору МА.
г). Безиндикаторный эксперт, с усреднением позиции, проверяющий параметры позиции только один раз на открытии нового бара. Пример - усредняющий эксперт.
3. Реализация класса эксперта
Создадим класс по всем выше описанным правилам и требованиям, который будет являться основой всех будущих экспертов.
Минимум функциональности, которая должна быть в классе CExpertAdvisor, выглядит следующим образом:
1. Инициализация:
- Регистрация индикаторов;
- Установка начальных значений параметров;
- Настройка на требуемый символ и таймфрейм.
2. Функции получения сигналов
- Разрешенное время работы (торгуемые интервалы);
- Определение сигнала для открытия/закрытия позиций или ордеров;
- Определение фильтра (тренда, времени и т.д.).
- Запуск, остановка таймера.
3. Сервисные функции
- Вычисление цены открытия, уровни SL и TP, объем ордера;
- Отправка торговых запросов (открытие, закрытие, модификация).
4. Торговые модули
- Обработка сигналов, фильтров;
- Контроль позиций и ордеров;
- Работа в функциях эксперта: OnTrade(), OnTimer(), OnTester(), OnChartEvent().
5. Деинициализация
- Вывод сообщений, отчетов;
- Чистка графика, выгрузка индикаторов.
Все функции класса разбиваются на три группы. Общая схема вложенности функций и их описание представлены ниже.
Рисунок 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 появились новые возможности обработки событий, расширяется структура управления торговым процессом. Но все это богатство становится действительно мощным инструментом в работе только в том случае, если вы будете правильно составлять свои торговые алгоритмы.
В статье изложены три главных принципа их создания - событийность, абстрактность, модульность. Вы намного облегчите себе жизнь в океане торговых систем, основывая свои эксперты на этих "трёх китах".
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
а чем работа по таймеру отличается от работы по по тикам?
Не стоит смешивать мух с котлетами, для работы нужен и таймер и тики (главное правильно их использовать).
Таймер нужен для выполнения периодических действий, еще там желательно выполнять часть кода мультивалютника (второе рекомендуется, но не обязательно).
PS
Таймер также интересно использовать для отслеживания состояния терминала. К примеру наличие коннекта...
Рекомендую уйти вот от такой конструкции
так как обработка предыдущего тика может занять достаточное количество времени чтобы пропустить приход первого тика нового бара
соответственно возможно пропустить открытие.
Лучше привязываться к времени открытия бара, но для этого нужно где то сохранять предыдущее время к примеру нулевого бара чтобы потом сравнивать его с текущем временем нулевого бара
Если одинаковое - нового бара нет
Если отличается то в большую сторону то как минимум открыт новый (следующий) бар после чего сохраненное время нулевого бара инициализируем текущем временем нулевого бара.
Такая конструкция более надежная.
Рекомендую уйти вот от такой конструкции
так как обработка предыдущего тика может занять достаточное количество времени чтобы пропустить приход первого тика нового бара
соответственно возможно пропустить открытие.
Лучше привязываться к времени открытия бара, но для этого нужно где то сохранять предыдущее время к примеру нулевого бара чтобы потом сравнивать его с текущем временем нулевого бара
Если одинаковое - нового бара нет
Если отличается то в большую сторону то как минимум открыт новый (следующий) бар после чего сохраненное время нулевого бара инициализируем текущем временем нулевого бара.
Такая конструкция более надежная.
Я у себя так делал: