
Рецепты MQL5 - обработка пользовательских событий графика
Введение
Данная статья является логическим продолжением статьи Рецепты MQL5 - обработка типичных событий графика. В текущем материале предлагаю читателю рассмотреть методику работы с пользовательскими событиями графика. Будут представлены примеры создания и обработки пользовательских событий. При этом использоваться будет объектно-ориентированный инструментарий.
Хотелось бы отметить, что тема пользовательских событий обширна. Это тот случай, когда работа программиста и разработчика сопряжена с творчеством и креативом.
1. Пользовательское событие графика
Как гласит само название, такое событие задается пользователем. Программист сам решает, что, какую задачу или программный блок, облечь в событийную форму. Разработчик MQL5 позволяет создавать свои собственные события, что расширяет гибкость самого языка при реализации сложных алгоритмов.
Пользовательское событие является вторым возможным видом события графика. Первым выступает типичное событие. И хотя в Документации нет такого термина "типичное событие графика", все же предлагаю его использовать для обращения к первым 10 типам события графика.
Разработчик предлагает для обработки всех событий графика 1 перечисление - ENUM_CHART_EVENT.
Согласно Документации, существует 65535 идентификаторов пользовательских событий. Первый и последний идентификаторы пользовательских событий задаются явными значениями CHARTEVENT_CUSTOM и CHARTEVENT_CUSTOM_LAST, что в численном выражении равно 1000 и 66534 соответственно (рис.1).
Рис.1 Первый и последний идентификаторы пользовательских событий
Если автор дружит с арифметикой, то с учетом первого и последнего идентификаторов всего получим: 66534-1000+1=65535.
Прежде чем использовать пользовательские события, нужно их придумать. В этом смысле разработчик становится идейным вдохновителем и автором событийной концепции, которая потом реализовывается в виде алгоритма для будущего советника. Неплохо бы иметь некоторую классификацию пользовательских событий. Этот когнитивный метод позволит если не избавиться, то снизить степень неопределенности и добавить больше порядка в ход рассуждений.
Предлагаю рассмотреть такой критерий пользовательского события, как источник. К примеру, разработчик sergeev выдвинул идею прототипа торгового робота. Он разделяет все события на 3 группы (рис.2).
Рис.2 Группы источников пользовательских событий
Тогда, согласно этой общей идее, нужно разрабатывать пользовательские события, исходя из групповой принадлежности.
Давайте попробуем для начала "натворить" что-то несложное. Возьмем первую группу — индикаторные события. События, которые могут попасть в эту группу: создание и удаление индикатора, получение сигнала на открытие, получение сигнала на закрытие. Вторая группа — это события изменения состояния ордеров и позиций. Пусть в нашем примере эта группа будет включать: открытие позиции, закрытие позиции. Все предельно просто. И, пожалуй, самая сложная группа для формализации — это внешние события.
Возьмем 2 события: приостановление и возобновление торговли вручную.
Рис.3 Источники пользовательских событий
Дедуктивный метод (от общего к частному) позволит детализировать первичную схему (рис.3). Именно по этой схеме позже создадим типы событий в соответствующем классе (табл.1).
Табл.1 Пользовательские события
Может, на событийную концепцию данная Таблица и не "тянет", но начало положено. Представлю еще один подход. Общеизвестно, что модель абстрактной торговой системы состоит из трех подсистем — базовых модулей (рис.4).
Рис.4 Модель абстрактной торговой системы
Тогда пользовательские события на основании критерия "источник" можно классифицировать как события, возникающие в:
- сигнальной подсистеме;
- подсистеме сопровождения позиций;
- подсистеме управления капиталом.
Последняя может включать в себя, например, такие события как: достижение допустимого уровня просадки, увеличение торгового объема на заданный размер, увеличение процента лимита потерь и т.д.
2. Обработчик и генератор ChartEvent
Уделю несколько строк обработчику и генератору события графика. Что касается обработки пользовательского события графика, то принцип аналогичен обработке типичного события графика.
Обработчик — функция OnChartEvent(), принимает в качестве параметров четыре константы. Вероятно, что с помощью такого механизма разработчик реализовывал идею идентификации события и получения дополнительной информации о нем. На мой взгляд, очень компактный и удобный программный механизм.
Генерирует пользовательское событие графика функция EventChartCustom(). Причем можно создать событие как для "своего" графика, так и для "чужого". Пожалуй, самой интересной статьей о смысле своих и чужих графиков является Реализация мультивалютного режима в MetaTrader 5.
Некоторый диссонанс вижу в том, что идентификатор события относится к типу ushort в генераторе, тогда как в обработчике он принадлежит типу int. Наверное, логично было бы в обработчике тоже использовать тип данных ushort.
3. Класс пользовательского события
Итак, как я уже отмечал, автор советника должен сам позаботиться о событийной концепции. Попробуем поработать с событиями, представленными в Таблице 1. Сначала разберемся с базовым классом пользовательского события CEventBase и его потомками (рис.5).
Рис.5 Иерархия событийных классов
Сам базовый класс выглядит так:
//+------------------------------------------------------------------+ //| Class CEventBase. | //| Purpose: base class for a custom event | //| Derives from class CObject. | //+------------------------------------------------------------------+ class CEventBase : public CObject { protected: ENUM_EVENT_TYPE m_type; ushort m_id; SEventData m_data; public: void CEventBase(void) { this.m_id=0; this.m_type=EVENT_TYPE_NULL; }; void ~CEventBase(void){}; //-- bool Generate(const ushort _event_id,const SEventData &_data, const bool _is_custom=true); ushort GetId(void) {return this.m_id;}; private: virtual bool Validate(void) {return true;}; };
Тип события задается перечислением ENUM_EVENT_TYPE:
//+------------------------------------------------------------------+ //| A custom event type enumeration | //+------------------------------------------------------------------+ enum ENUM_EVENT_TYPE { EVENT_TYPE_NULL=0, // no event //--- EVENT_TYPE_INDICATOR=1, // indicator event EVENT_TYPE_ORDER=2, // order event EVENT_TYPE_EXTERNAL=3, // external event };
Еще члены-данные включают идентификатор события и структуру данных.
Метод Generate() базового класса CEventBase занимается генерированием события. Метод GetId() возвращает id события, а виртуальный метод Validate() будет проверять значение идентификатора события. Сначала я включил в состав класса метод обработки события. Но потом понял, что каждое событие уникально, и абстрактным методом тут не обойдешься. В общем, переложил эту задачу на плечи класса-обработчика пользовательских событий CEventProcessor.
4. Класс обработчика пользовательских событий
Предполагается, что класс CEventProcessor будет генерировать и обрабатывать 8 представленных событий. Члены-данные класса выглядят так:
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+----------------------------Data members--------------------------+ protected: ulong m_magic; //--- flags bool m_is_init; bool m_is_trade; //--- CEventBase *m_ptr_event; //--- CTrade m_trade; //--- CiMA m_fast_ema; CiMA m_slow_ema; //--- CButton m_button; bool m_button_state; //+------------------------------------------------------------------+ };
Перечень атрибутов достаточно пестрый: здесь есть флаги инициализации и торговли. Первый не позволит советнику торговать, если он неудачно стартовал. Второй проверяет разрешение пользователя на торговлю.
Также есть указатель на объект типа CEventBase, который как раз и работает с событиями разных типов с помощью полиморфизма. Объект торгового класса CTrade предоставляет доступ к торговым операциям.
Пара объектов типа CiMA облегчают обработку данных от индикаторов. Для упрощения примера я взял 2 мувинга. Они будут ловить торговый сигнал. Еще есть представитель класса "Кнопка". Он будет обслуживать "ручное" включение/отключение советника.
Методы класса разбил по принципу "модули-процедуры-функции-макросы":
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+-------------------------------Methods----------------------------+ public: //--- constructor/destructor void CEventProcessor(const ulong _magic); void ~CEventProcessor(void); //--- Modules //--- event generating bool Start(void); void Finish(void); void Main(void); //--- event processing void ProcessEvent(const ushort _event_id,const SEventData &_data); private: //--- Procedures void Close(void); void Open(void); //--- Functions ENUM_ORDER_TYPE CheckCloseSignal(const ENUM_ORDER_TYPE _close_sig); ENUM_ORDER_TYPE CheckOpenSignal(const ENUM_ORDER_TYPE _open_sig); bool GetIndicatorData(double &_fast_vals[],double &_slow_vals[]); //--- Macros void ResetEvent(void); bool ButtonStop(void); bool ButtonResume(void); };
Среди модулей есть три, которые только генерируют события: стартовый — Start(), финишный — Finish(), главный — Main(). И четвертый модуль ProcessEvent() является как обработчиком событий, так и генератором.
4.1 Стартовый модуль
Предполагается, что модуль будет вызываться в обработчике OnInit().
//+------------------------------------------------------------------+ //| Start module | //+------------------------------------------------------------------+ bool CEventProcessor::Start(void) { //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+1 event if(this.m_ptr_event.Generate(1,data)) //--- create a button if(this.m_button.Create(0,"Start_stop_btn",0,25,25,150,50)) if(this.ButtonStop()) { this.m_button_state=false; return true; } } //--- return false; }
В модуле создается указатель на объект индикаторного события. Затем генерируется событие "Создание индикатора". И в завершение создается кнопка. Ее состояние переводится в режим "Stop". Это означает, что последующее нажатие на кнопку прервет работу советника.
В определении метода еще задействована структура SEventData. Это простой контейнер для параметров, передаваемых генератору пользовательского события. Здесь будет заполнено только одно поле структуры — типа long. Сохраним в него магик советника.
4.2 Финишный модуль
Предполагается, что модуль будет вызываться в обработчике OnDeinit().
//+------------------------------------------------------------------+ //| Finish module | //+------------------------------------------------------------------+ void CEventProcessor::Finish(void) { //--- reset the event object this.ResetEvent(); //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+2 event bool is_generated=this.m_ptr_event.Generate(2,data,false); //--- process CHARTEVENT_CUSTOM+2 event if(is_generated) this.ProcessEvent(CHARTEVENT_CUSTOM+2,data); } }
В нем сбрасывается предыдущий указатель на событие, и генерируется событие "Удаление индикатора". Отмечу такой нюанс: если в обработчике OnDeinit() сгенерировать пользовательское событие, то получим ошибку времени выполнения 4001 (неожиданная внутренняя ошибка). Поэтому для этого метода генерация и обработка события осуществляются в пределах метода без вызова OnChartEvent().
Посредством структуры SEventData также сохраним только магик советника.
4.3 Главный модуль
Предполагается, что модуль будет вызываться в обработчике OnTick().
//+------------------------------------------------------------------+ //| Main module | //+------------------------------------------------------------------+ void CEventProcessor::Main(void) { //--- a new bar object static CisNewBar newBar; //--- if initialized if(this.m_is_init) //--- if not paused if(this.m_is_trade) //--- if a new bar if(newBar.isNewBar()) { //--- close module this.Close(); //--- open module this.Open(); } }
В этом модуле вызываются процедуры Open() и Close(). Первая может сгенерировать событие "Получение сигнала на открытие", а вторая — "Получение сигнала на закрытие". Текущая версия модуля полноценно работает при появлении нового бара. Класс для поиска нового бара определен Константином Груздевым.
4.4 Модуль обработки событий
Предполагается, что модуль будет вызываться в обработчике OnChartEvent(). По своему объему кода, да и по функциональной нагрузке этот модуль является самым большим.
//+------------------------------------------------------------------+ //| Process event module | //+------------------------------------------------------------------+ void CEventProcessor::ProcessEvent(const ushort _event_id,const SEventData &_data) { //--- check event id if(_event_id==CHARTEVENT_OBJECT_CLICK) { //--- button click if(StringCompare(_data.sparam,this.m_button.Name())==0) { //--- button state bool button_curr_state=this.m_button.Pressed(); //--- to stop if(button_curr_state && !this.m_button_state) { if(this.ButtonResume()) { this.m_button_state=true; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+7 event ushort curr_id=7; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- to resume else if(!button_curr_state && this.m_button_state) { if(this.ButtonStop()) { this.m_button_state=false; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+8 event ushort curr_id=8; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } } } //--- user event else if(_event_id>CHARTEVENT_CUSTOM) { long magic=_data.lparam; ushort curr_event_id=this.m_ptr_event.GetId(); //--- check magic if(magic==this.m_magic) //--- check id if(curr_event_id==_event_id) { //--- process the definite user event switch(_event_id) { //--- 1) indicator creation case CHARTEVENT_CUSTOM+1: { //--- create a fast ema if(this.m_fast_ema.Create(_Symbol,_Period,21,0,MODE_EMA,PRICE_CLOSE)) if(this.m_slow_ema.Create(_Symbol,_Period,55,0,MODE_EMA,PRICE_CLOSE)) if(this.m_fast_ema.Handle()!=INVALID_HANDLE) if(this.m_slow_ema.Handle()!=INVALID_HANDLE) { this.m_trade.SetExpertMagicNumber(this.m_magic); this.m_trade.SetDeviationInPoints(InpSlippage); //--- this.m_is_init=true; } //--- break; } //--- 2) indicator deletion case CHARTEVENT_CUSTOM+2: { //---release indicators bool is_slow_released=IndicatorRelease(this.m_fast_ema.Handle()); bool is_fast_released=IndicatorRelease(this.m_slow_ema.Handle()); if(!(is_slow_released && is_fast_released)) { //--- to log? if(InpIsLogging) Print("Failed to release the indicators!"); } //--- reset the event object this.ResetEvent(); //--- break; } //--- 3) check open signal case CHARTEVENT_CUSTOM+3: { MqlTick last_tick; if(SymbolInfoTick(_Symbol,last_tick)) { //--- signal type ENUM_ORDER_TYPE open_ord_type=(ENUM_ORDER_TYPE)_data.dparam; //--- double open_pr,sl_pr,tp_pr,coeff; open_pr=sl_pr=tp_pr=coeff=0.; //--- if(open_ord_type==ORDER_TYPE_BUY) { open_pr=last_tick.ask; coeff=1.; } else if(open_ord_type==ORDER_TYPE_SELL) { open_pr=last_tick.bid; coeff=-1.; } sl_pr=open_pr-coeff*InpStopLoss*_Point; tp_pr=open_pr+coeff*InpStopLoss*_Point; //--- to normalize prices open_pr=NormalizeDouble(open_pr,_Digits); sl_pr=NormalizeDouble(sl_pr,_Digits); tp_pr=NormalizeDouble(tp_pr,_Digits); //--- open the position if(!this.m_trade.PositionOpen(_Symbol,open_ord_type,InpTradeLot,open_pr, sl_pr,tp_pr)) { //--- to log? if(InpIsLogging) Print("Failed to open the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+5 event ushort curr_id=5; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- break; } //--- 4) check close signal case CHARTEVENT_CUSTOM+4: { if(!this.m_trade.PositionClose(_Symbol)) { //--- to log? if(InpIsLogging) Print("Failed to close the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+6 event ushort curr_id=6; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } //--- break; } //--- 5) position opening case CHARTEVENT_CUSTOM+5: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_IN) { //--- to log? if(InpIsLogging) { Print("\nNew position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 6) position closing case CHARTEVENT_CUSTOM+6: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_OUT) { //--- to log? if(InpIsLogging) { Print("\nClosed position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 7) stop trading case CHARTEVENT_CUSTOM+7: { datetime stop_time=(datetime)_data.dparam; //--- this.m_is_trade=false; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is stopped at: %s", TimeToString(stop_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } //--- 8) resume trading case CHARTEVENT_CUSTOM+8: { datetime resume_time=(datetime)_data.dparam; this.m_is_trade=true; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is resumed at: %s", TimeToString(resume_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } } } } }
Он состоит из двух частей. Первая занимается обработкой событий, связанных с кликом по объекту "Кнопка". Если щелчок имеет место, то будет сгенерировано внешнее пользовательское событие, которое чуть позже сам обработчик и обслужит.
Вторая часть призвана реагировать на сгенерированные пользовательские события. В ней есть 2 блока, где после обработки соответствующего события происходит создание нового. Первый блок — это обслуживание события "Получение сигнала на открытие". Его успешная обработка порождает новое ордерное событие "Открытие позиции". Второй блок — это обслуживание события "Получение сигнала на закрытие". Если сигнал обработан, то появляется событие "Закрытие позиции".
В качестве примера использования класса CEventProcessor приведу советник CustomEventProcessor.mq5. В нем все сделано так, чтобы создавать события и реагировать на события. Применение ООП позволило конечный файл исходного кода написать достаточно компактно. Код советника представлен в архиве.
Не думаю, что всегда нужно обращаться к механизму пользовательского события. Наверно, какие-то мелкие, неважные и несобытийные для стратегии вещи можно облечь в другую форму.
Заключение
В своей статье я попытался продемонстрировать принципы работы с пользовательскими событиями в среде MQL5. Надеюсь, что статейный материал вызовет интерес не только у начинающих программистов.
Да, хотел бы с радостью заметить, что язык MQL5 развивается. Возможно, что совсем скоро появятся шаблоны классов, а когда-нибудь и указатели на функции. Тогда сможем запрограммировать полноценный делегат, указывающий на метод произвольного объекта.
Исходные файлы из архива удобно расположить в папке проектов. В моем случае это папка MQL5\Projects\ChartUserEvent.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Делегатов нет в языке, да и вообще много чего нет. Даже обработчика ошибок. А ведь чужой класс - это черный ящик, смешно читать в данном контексте комменты разработчиков языка, что надо все ошибки "ловить" при написании. Разочаровался в языке =(