
MQL5 Cookbook: Handling Custom Chart Events
Introduction
This article is a logical continuation of the article MQL5 Cookbook: Handling Typical Chart Events. It covers methods of work with custom chart events. Here the reader can find examples of developing and handling of custom events. All the ideas discussed in this article were implemented with object-oriented tools.
As the theme of custom events is rather broad, it is the case when a programmer and developer can introduce creativity to their work.
1. Custom Chart Event
It is clear that this event is defined by the user. It is up to a programmer to decide what exactly and which task or a program block could take the form of an event. The MQL5 developers can create their own events, which expands the language capabilities for implementation of complex algorithms.
A custom event is the second possible type of a chart event. The first one is a typical event. Although there is no such a term as "typical chart event" in the Documentation, I still suggest using it for the first ten types of chart events.
The developers suggest only one enumeration for all chart events—ENUM_CHART_EVENT.
According to the Documentation, there are 65535 identifiers of custom events. The first and the last identifiers of the custom events are set by the the explicit values of CHARTEVENT_CUSTOM and CHARTEVENT_CUSTOM_LAST, which is numerically equal to 1000 and 66534 accordingly (Fig.1).
Fig.1 The first and the last identifiers of custom events
Simple calculations considering the first and the last identifiers will produce: 66534-1000+1=65535.
Before using custom events, they have to be designed first. In this sense a developer becomes a mastermind and the author of the event concept, which is then gets implemented as an algorithm for the future Expert. It would be useful to have a classification of custom events. This cognitive method will not allow to get rid of the ambiguity but will certainly reduce the degree of it and will arrange the line of reasoning.
Let us consider such a criterion of a custom event as the source. For example, developer sergeev suggested an idea of a trading robot prototype. He divides all events into three groups (Fig.2).
Fig.2 Groups of custom event sources
Then, according to this main idea, the custom events are to be developed based on their group affiliation.
Let us try to do something simple to start with. At first we shall take the first group, which comprises indicator events. Events that can belong to this group are: creation and deletion of an indicator, receiving a signal for opening and closing of a position. The second group includes events of changing the state of orders and positions. In our example, opening and closing of positions will be in this group. It is all very simple. And, finally, the most complex group for formalization is a group of external events.
Let us take two events: enabling and disabling the manual trading.
Fig.3 Sources of custom events
The primary pattern can be established through the deductive method (from the general to the special) (Fig.3). This is the very pattern we are going to use later on for creating types of events in the correspondent class (Table 1).
Table 1 Custom events
This table can not be called an "event concept" yet, but it is a start. Here is another approach. It is common knowledge that a model of an abstract trading system consists of three subsystems—basic modules (Fig.4).
Fig.4 Model of an abstract trading system
Custom events based on the "source" criterion can be classified as events generated in:
- the signal subsystem;
- subsystem of trailing open positions;
- subsystem of money management.
The latter, for instance, can include such events as reaching the permissible drawdown level, increasing a trading volume by a set value, increasing percentage of a loss limit, etc.
2. Handler and Generator of ChartEvent
The following few lines will be dedicated to the handler and generator of chart events. As for handling a custom chart event, its principle is similar to the one of handling of a typical chart event.
A handler, the OnChartEvent() function, takes four constants as parameters. Apparently, the developers used this mechanism to implement the idea of identifying an event and obtaining additional information about it. In my opinion it is a very compact and convenient program mechanism.
Function EventChartCustom() generates a custom chart event. Remarkably, a custom chart event can be created for "own" chart and for an "alien" one. I think, the most interesting article about the meaning of own and alien charts is The Implementation of a Multi-currency Mode in MetaTrader 5.
In my opinion, there is a discord in the fact that the event identifier is of the ushort type in the generator, while in the handler it is of the int type. It would be logical to use the ushort data type in the handler too.
3. Class of Custom Event
As I mentioned before, the event concept is up to the Expert developer. Now we are going to work with the events from the Table 1. At first we will sort the class of the custom event CEventBase and its derivatives (Fig.5).
Fig.5 Hierarchy of event classes
The basic class looks as follows:
//+------------------------------------------------------------------+ //| 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;}; };
The event type is set by the ENUM_EVENT_TYPE enumeration:
//+------------------------------------------------------------------+ //| 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 };
The data members comprise of the event identifier and the data structure.
The Generate() method of the CEventBase basic class deals with generating an event. The GetId() method returns the event id and the virtual method Validate() checks the event identifier value. At first I included the method of event handling into the class, but later I realized that every event is unique and an abstract method is not sufficient here. I ended up delegating this task to the CEventProcessor class that handles custom events.
4. Custom Event Handler Class
The CEventProcessor class is supposed to generate and handle eight presented events. Data members of the class look like:
//+------------------------------------------------------------------+ //| 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; //+------------------------------------------------------------------+ };
Among the list of attributes there are flags of initialization and trade. The first one will not allow the EA to trade if it failed to start correctly. The second one checks permission for trading.
There is also the pointer to the object of the CEventBase type, which works with events of different types using polymorphism. An instance of the CTrade class provides access to trading operations.
Objects of the CiMA type facilitate handling of data received from indicators. To simplify the example, I took two Moving Averages which are going to receive a trading signal. There is also an instance of the "CButton" class that will be used for manual enabling/disabling of the EA.
The methods of the class were divided by the "modules – procedures – functions – macros" principle:
//+------------------------------------------------------------------+ //| 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); };
Among the modules there are three that only generate events: the starting one—Start(), the finishing one—Finish() and the main one—Main(). The fourth module ProcessEvent() though is both an event handler and a generator.
4.1 Starting Module
This module is designed to be called in the OnInit() handler.
//+------------------------------------------------------------------+ //| 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; }
A pointer to the indicator event object is created in this module. Then the "Indicator creation" event is generated. A button is the last one to be created. It is switched to the "Stop" mode. It means that should the button be pressed, the Expert would stop working.
The SEventData structure is also involved in this method definition. It is a simple container for parameters passed on to the generator of the custom event. Only one field of the structure will be filled here—it is the field of the long type. It will hold EA's magic number.
4.2 Finishing Module
This module is supposed to be called in the OnDeinit() handler.
//+------------------------------------------------------------------+ //| 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); } }
Here the previous event pointer is cleared and the "Indicator deletion" event is generated. I must note that if a custom event is generated in the OnDeinit() handler, you will get a runtime error 4001 (unexpected external error). Therefore, event generation and handling are carried out within this method without calling OnChartEvent().
Again, EA's magic number will be stored using the SEventData structure.
4.3 Main Module
This module is supposed to be called in the OnTick() handler.
//+------------------------------------------------------------------+ //| 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(); } }
Procedures Open() and Close() are called in this module. The first procedure can generate the "Receiving a signal for opening" event, and the second one—"Receiving a signal for closing" event. The current version of the module is fully functional at a new bar appearance. A class for detecting a new bar was described by Konstantin Gruzdev.
4.4 Module of Event Handling
This module is supposed to be called in the OnChartEvent() handler. This module is the largest in terms of size and functionality.
//+------------------------------------------------------------------+ //| 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; } } } } }
It consists of two parts. The first one is handling events connected with a click on the "Button" object. This click will generate an external custom event, which will be dealt with by the handler later.
The second part is designed for processing of generated custom events. It contains two blocks, where after a relevant event has been handled, a new one is generated. The "Receiving a signal for opening" event is processed in the first block. Its successful handling generates a new order event "Opening of a position". The "Receiving a signal for closing" event is processed in the second block. If the signal is handled, then the "Closing of a position" event takes place.
The Expert CustomEventProcessor.mq5 is a good example of using the CEventProcessor class. The EA was designed for creating events and responding to them appropriately. With OPP paradigm, we were able to minimize the source code to a fewer number of lines. EA source code can be found in attachment to this article.
To my mind, there is no need to refer to the mechanism of a custom event every time. There are plenty of minor, insignificant and non-eventful in terms of strategy things that can have a different form.
Conclusion
In this article I tried to illustrate principles of working with custom events in the MQL5 environment. I hope that the ideas covered in this article will be of interest for programmers with different experience, not only novice ones.
I am glad that the MQL5 language is developing. Probably, in the nearest future there will be class templates and may be pointers to functions. Then we shall be able to write a fully-fledged delegate pointing to a method of an arbitrary object.
Source files from the archive can be placed into a project folder. In my case, it is the folder MQL5\Projects\ChartUserEvent.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/1163





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use