
Cross-Platform Expert Advisor: The CExpertAdvisor and CExpertAdvisors Classes
Table of Contents
- Introduction
- The Expert Advisor Class
- Initialization
- New Bar Detection
- OnTick Handler
- Expert Advisors Container
- Persistence of Data
- Examples
- Final Remarks
- Conclusion
Introduction
In earlier articles regarding this topic, the expert advisor examples featured have their components scattered all over the expert advisor main header file through the use of custom-defined functions. This article features the classes CExpertAdvisor and CExpertsAdvisors, which aim at the creating a more harmonious interaction between the various components of a cross-platform expert advisor. It also addresses some common problems usually encountered in expert advisors, such as loading and saving volatile data, and new bar detection.
The Expert Advisor Class
The CExpertAdvisorBase class is shown in the following code snippet. At this point, most of the differences between MQL4 and MQL5 are handled by the other class objects that were discussed in previous articles.
class CExpertAdvisorBase : public CObject { protected: //--- trade parameters bool m_active; string m_name; int m_distance; double m_distance_factor_long; double m_distance_factor_short; bool m_on_tick_process; //--- signal parameters bool m_every_tick; bool m_one_trade_per_candle; datetime m_last_trade_time; string m_symbol_name; int m_period; bool m_position_reverse; //--- signal objects CSignals *m_signals; //--- trade objects CAccountInfo m_account; CSymbolManager m_symbol_man; COrderManager m_order_man; //--- trading time objects CTimes *m_times; //--- candle CCandleManager m_candle_man; //--- events CEventAggregator *m_event_man; //--- container CObject *m_container; public: CExpertAdvisorBase(void); ~CExpertAdvisorBase(void); virtual int Type(void) const {return CLASS_TYPE_EXPERT;} //--- initialization bool AddEventAggregator(CEventAggregator*); bool AddMoneys(CMoneys*); bool AddSignal(CSignals*); bool AddStops(CStops*); bool AddSymbol(const string); bool AddTimes(CTimes*); virtual bool Init(const string,const int,const int,const bool,const bool,const bool); virtual bool InitAccount(void); virtual bool InitCandleManager(void); virtual bool InitEventAggregator(void); virtual bool InitComponents(void); virtual bool InitSignals(void); virtual bool InitTimes(void); virtual bool InitOrderManager(void); virtual bool Validate(void) const; //--- container void SetContainer(CObject*); CObject *GetContainer(void); //--- activation and deactivation bool Active(void) const; void Active(const bool); //--- setters and getters string Name(void) const; void Name(const string); int Distance(void) const; void Distance(const int); double DistanceFactorLong(void) const; void DistanceFactorLong(const double); double DistanceFactorShort(void) const; void DistanceFactorShort(const double); string SymbolName(void) const; void SymbolName(const string); //--- object pointers CAccountInfo *AccountInfo(void); CStop *MainStop(void); CMoneys *Moneys(void); COrders *Orders(void); COrders *OrdersHistory(void); CStops *Stops(void); CSignals *Signals(void); CTimes *Times(void); //--- order manager string Comment(void) const; void Comment(const string); bool EnableTrade(void) const; void EnableTrade(bool); bool EnableLong(void) const; void EnableLong(bool); bool EnableShort(void) const; void EnableShort(bool); int Expiration(void) const; void Expiration(const int); double LotSize(void) const; void LotSize(const double); int MaxOrdersHistory(void) const; void MaxOrdersHistory(const int); int Magic(void) const; void Magic(const int); uint MaxTrades(void) const; void MaxTrades(const int); int MaxOrders(void) const; void MaxOrders(const int); int OrdersTotal(void) const; int OrdersHistoryTotal(void) const; int TradesTotal(void) const; //--- signal manager int Period(void) const; void Period(const int); bool EveryTick(void) const; void EveryTick(const bool); bool OneTradePerCandle(void) const; void OneTradePerCandle(const bool); bool PositionReverse(void) const; void PositionReverse(const bool); //--- additional candles void AddCandle(const string,const int); //--- new bar detection void DetectNewBars(void); //-- events virtual bool OnTick(void); virtual void OnChartEvent(const int,const long&,const double&,const string&); virtual void OnTimer(void); virtual void OnTrade(void); virtual void OnDeinit(const int,const int); //--- recovery virtual bool Save(const int); virtual bool Load(const int); protected: //--- candle manager virtual bool IsNewBar(const string,const int); //--- order manager virtual void ManageOrders(void); virtual void ManageOrdersHistory(void); virtual void OnTradeTransaction(COrder*) {} virtual datetime Time(const int); virtual bool TradeOpen(const string,const ENUM_ORDER_TYPE,double,bool); //--- symbol manager virtual bool RefreshRates(void); //--- deinitialization void DeinitAccount(void); void DeinitCandle(void); void DeinitSignals(void); void DeinitSymbol(void); void DeinitTimes(void); };
Most of the class methods declared within this class serve as wrappers to methods of its components. The key methods in this class will be discussed in later sections.
Initialization
During the initialization phase of the expert advisor, our primary goal is to instantiate the objects needed by the trading strategy (e.g. money management, signals, etc.) and then integrate them with the instance of CExpertAdvisor, which would also need to be created during OnInit. With this goal, when any of the event functions is triggered within the expert advisor, all we need to supply is a single line of code calling the appropriate handler or method of the CExpertAdvisor instance. This is very similar to the way the MQL5 Standard Library CExpert is used.
After the creation of an instance of CExpertAdvisor, the next method to call is its Init method. The code of the said method is shown below:
bool CExpertAdvisorBase::Init(string symbol,int period,int magic,bool every_tick=true,bool one_trade_per_candle=true,bool position_reverse=true) { m_symbol_name=symbol; CSymbolInfo *instrument; if((instrument=new CSymbolInfo)==NULL) return false; if(symbol==NULL) symbol=Symbol(); if(!instrument.Name(symbol)) return false; instrument.Refresh(); m_symbol_man.Add(instrument); m_symbol_man.SetPrimary(m_symbol_name); m_period=(ENUM_TIMEFRAMES)period; m_every_tick=every_tick; m_order_man.Magic(magic); m_position_reverse=position_reverse; m_one_trade_per_candle=one_trade_per_candle; CCandle *candle=new CCandle(); candle.Init(instrument,m_period); m_candle_man.Add(candle); Magic(magic); return false; }
Here, we create the instances of most components that are often found in trading strategies. This includes the symbol or instrument to use (which has to be translated to a type of object), and the default period or timeframe. It also contains the rule on whether or not it should operate its core tasks at every new tick or at the first tick of the each candle only, whether or not it should limit a maximum of one trade per candle only (to prevent multiple entries on the same candle), and whether it should reverse its position at opposite signal (close existing trades and re-enter based on the new signal).
At the end of the OnInit function, the instance of CExpertAdvisor would have to make a call to its InitComponents method. The following code shows the the said method of CExpertBase:
bool CExpertAdvisorBase::InitComponents(void) { if(!InitSignals()) { Print(__FUNCTION__+": error in signal initialization"); return false; } if(!InitTimes()) { Print(__FUNCTION__+": error in time initialization"); return false; } if(!InitOrderManager()) { Print(__FUNCTION__+": error in order manager initialization"); return false; } if(!InitCandleManager()) { Print(__FUNCTION__+": error in candle manager initialization"); return false; } if(!InitEventAggregator()) { Print(__FUNCTION__+": error in event aggregator initialization"); return false; } return true; }
In this method, the Init method of each of the components of the expert advisor instance is called. It is also through this method where the Validate methods of each component is called to see if their settings would pass validation.
New Bar Detection
Some trading strategies require only to operate at the first tick of a new candle. There are many ways to implement this feature. One of this is the comparison of the open time and open price of the current candle to their previous states, which is the method implemented in the CCandle class. The following code shows the declaration for CCandleBase, from which CCandle is based:
class CCandleBase : public CObject { protected: bool m_new; bool m_wait_for_new; bool m_trade_processed; int m_period; bool m_active; MqlRates m_last; CSymbolInfo *m_symbol; CEventAggregator *m_event_man; CObject *m_container; public: CCandleBase(void); ~CCandleBase(void); virtual int Type(void) const {return(CLASS_TYPE_CANDLE);} virtual bool Init(CSymbolInfo*,const int); virtual bool Init(CEventAggregator*); CObject *GetContainer(void); void SetContainer(CObject*); //--- setters and getters void Active(bool); bool Active(void) const; datetime LastTime(void) const; double LastOpen(void) const; double LastHigh(void) const; double LastLow(void) const; double LastClose(void) const; string SymbolName(void) const; int Timeframe(void) const; void WaitForNew(bool); bool WaitForNew(void) const; //--- processing virtual bool TradeProcessed(void) const; virtual void TradeProcessed(bool); virtual void Check(void); virtual void IsNewCandle(bool); virtual bool IsNewCandle(void) const; virtual bool Compare(MqlRates &) const; //--- recovery virtual bool Save(const int); virtual bool Load(const int); };
The checking of the presence of a new candle on the chart is done through its Check method, which is shown below:
CCandleBase::Check(void) { if(!Active()) return; IsNewCandle(false); MqlRates rates[]; if(CopyRates(m_symbol.Name(),(ENUM_TIMEFRAMES)m_period,1,1,rates)==-1) return; if(Compare(rates[0])) { IsNewCandle(true); TradeProcessed(false); m_last=rates[0]; } }
If checking for a new bar, the expert advisor instance should always call this method every tick. The coder is then free to extend CCxpertAdvisor so that it can perform additional tasks when a new candle appears on the chart.
As shown in the code above, the actual comparison of the open time and open price of the bar is done through the Compare method of the class, which is shown in the following code:
bool CCandleBase::Compare(MqlRates &rates) const { return (m_last.time!=rates.time || (m_last.open/m_symbol.TickSize())!=(rates.open/m_symbol.TickSize()) || (!m_wait_for_new && m_last.time==0)); }
This method of checking for the existence of a new bar depends on three conditions. Satisfying at least one will guarantee a result of true, which indicates the presence of a new candle on the chart:
- The last recorded open time is not equal to the open time of the current bar
- The last recorded open price is not equal to the open price of the current bar
- The last recorded open time is zero and a new bar does not have to be the first tick for that bar
The first two conditions involves the direct comparison of the rates of the current bar to the previous recorded state. The third condition only applies to the very first tick that the expert advisor will encounter. As soon as an expert advisor is loaded on a chart, it does not have yet any previous record of the rates (open time and open price), and so the last recorded open time would be zero. Some traders consider this bar as a new bar for their expert advisors, while others prefer to have the expert advisor wait for an actual new bar to appear on the chart after the initialization of the expert advisor.
Similar to other types of classes discussed previously, the class CCandle would also have its container, CCandleManager. The following code shows the declaration of CCandleManagerBase:
class CCandleManagerBase : public CArrayObj { protected: bool m_active; CSymbolManager *m_symbol_man; CEventAggregator *m_event_man; CObject *m_container; public: CCandleManagerBase(void); ~CCandleManagerBase(void); virtual int Type(void) const {return(CLASS_TYPE_CANDLE_MANAGER);} virtual bool Init(CSymbolManager*,CEventAggregator*); virtual bool Add(const string,const int); CObject *GetContainer(void); void SetContainer(CObject *container); bool Active(void) const; void Active(bool active); virtual void Check(void) const; virtual bool IsNewCandle(const string,const int) const; virtual CCandle *Get(const string,const int) const; virtual bool TradeProcessed(const string,const int) const; virtual void TradeProcessed(const string,const int,const bool) const; //--- recovery virtual bool Save(const int); virtual bool Load(const int); };
An instance of CCandle is created based on the name of the instrument and the timeframe. Having CCandleManager would make it easier for an expert advisor to track multiple charts for a given instrument, for example, having the capacity to check the occurrence of a new candle on EURUSD M15 and EURUSD H1 in the same expert advisor. Instances of CCandle that have the same symbol and timeframe are redundant and should be avoided. When looking for a certain instance of CCandle, one should simply call the appropriate method found on CCandleManager and specify the symbol and timeframe. CCandleManager, in turn, would look for the appropriate CCandle instance and call the intended method.
Aside from checking the occurrence of a new candle, CCandle and CCandleManager serve another purpose: checking if a trade has been entered for a given symbol and timeframe within an expert advisor. The recent trade on a symbol can be checked, but not for a timeframe. The toggle for this flag should be set or reset by the instance of the CExpertAdvisor itself, when needed. For both classes, toggle can be set using the TradeProcessed method.
For the candle manager, the TradeProcessed methods (getter and setter) only deals with finding the instance of CCandle requested and applying the appropriate value:
bool CCandleManagerBase::TradeProcessed(const string symbol,const int timeframe) const { CCandle *candle=Get(symbol,timeframe); if(CheckPointer(candle)) return candle.TradeProcessed(); return false; }
For CCandle, the process involves the assigning of a new value to one of its its class members, m_trade_processed. The following methods deal with the setting of the value of the said class member:
bool CCandleBase::TradeProcessed(void) const { return m_trade_processed; } CCandleBase::TradeProcessed(bool value) { m_trade_processed=value; }
OnTick Handler
The OnTick method of CExpertAdvisor is the most used function within the class. It is from this method where most of the action takes place. The core operation of this method is shown in the following diagram:
The process begins by toggling the tick flag of the expert advisor. This is to ensure that double processing of a tick cannot occur. The OnTick method of CExpertAdvisor is ideally called only within the OnTick event function, but it can also be called through other means, such as OnChartEvent. In the absence of this flag, if the OnTick method of the class is called while it is still processing an earlier tick, a tick may be processed more than once, and if the tick would generate a trade, this would often result to a duplicate trade.
The refreshing of data is also necessary, as this ensures that the expert advisor has access to the most recent market data, and will not reprocess an earlier tick. If the expert advisor fails to refresh the data, it would reset the tick process flag, terminate the method, and wait for a new tick.
The next steps are the detection of new bars and checking of trade signals. The checking for this is done every tick by default. However, it is possible to extend this method so that it only checks signals when a new signal is detected (to speed up processing time, especially during backtesting and optimization).
The class also provides a member, m_position_reverse, which is intended to reverse position(s) opposite the current signal. The reversal performed here is only for the neutralization of the current position(s). In MetaTrader 4 and MetaTrader 5 hedging mode, this deals with the exit of the trades that are opposite the current signal (those going with the current signal will not be exited). In MetaTrader 5 netting mode, there can only be one position at any given time, so the expert advisor will enter a new position of equal volume and opposite to that of the current position.
The trade signal is mostly check using m_signals, but other factors such as trading on new bar only, and time filters, can also prevent the expert advisor from executing a new trade. Only when all the conditions are satisfied will the EA be able to enter a new trade.
At the end of the processing of the tick, the expert advisor will set the tick flag to false, and is then allowed to process another tick.
Expert Advisors Container
Similar to other class objects discussed in previous articles, the CExpertAdvisor class would also have its designated container, which is CExpertAdvisors. The following code shows the declaration for its base class, CExpertAdvisorsBase:
class CExpertAdvisorsBase : public CArrayObj { protected: bool m_active; int m_uninit_reason; CObject *m_container; public: CExpertAdvisorsBase(void); ~CExpertAdvisorsBase(void); virtual int Type(void) const {return CLASS_TYPE_EXPERTS;} virtual int UninitializeReason(void) const {return m_uninit_reason;} //--- getters and setters void SetContainer(CObject *container); CObject *GetContainer(void); bool Active(void) const; void Active(const bool); int OrdersTotal(void) const; int OrdersHistoryTotal(void) const; int TradesTotal(void) const; //--- initialization virtual bool Validate(void) const; virtual bool InitComponents(void) const; //--- events virtual void OnTick(void); virtual void OnChartEvent(const int,const long&,const double&,const string&); virtual void OnTimer(void); virtual void OnTrade(void); virtual void OnDeinit(const int,const int); //--- recovery virtual bool CreateElement(const int); virtual bool Save(const int); virtual bool Load(const int); };
This container primarily mirrors the public methods found in the CExpertAdvisor class. An example of this is the OnTick handler. The method simply iterates on each instance of CExpertAdvisor to call its OnTick method:
void CExpertAdvisorsBase::OnTick(void) { if(!Active()) return; for(int i=0;i<Total();i++) { CExpertAdvisor *e=At(i); e.OnTick(); } }
With this container it is possible to store multiple instances of CExpertAdvisor. This is probably the only way to run multiple expert advisors on a single chart instance. Simply initialize multiple instances of CExpertAdvisor, store their pointers under a single CExpertAdvisors container, and then use the container's OnTick method to trigger the OnTick methods of each CExpertAdvisor instance. The same thing can be also done with each instance of the CExpert class of the MQL5 Standard Library using the CArrayObj class or its heirs.
Persistence of Data
Some data used in an instance of CExpertAdvisor only reside in computer memory. Normally, the data are often stored in the platform and the expert advisor gets the needed data from the platform itself through a function call. However, for data created dynamically while the expert advisor is running, this is not usually the case. When the OnDeinit event is triggered on an expert advisor, the expert advisor destroys all objects, and thus loses the data.
OnDeinit can be triggered in a number of ways, such as closing the entire trading platform (MetaTrader 4 or MetaTrader 5), unloading the expert advisor from the chart, or even the act of recompiling the expert advisor source code. The full list of possible events that can trigger deinitialization can be found using the UninitializeReason function. When an expert advisor loses access on those data, it may behave as if it was just loaded on the chart for the first time.
Most of the volatile data in the CExpertAdvisor class can be found in one of its members, which is an instance of COrderManager. This is where instances of COrder and COrderStop (and descendants) are created as the expert advisor performs its usual routine. Since these instances are created dynamically during OnTick, they are not recreated when the expert advisor reinitializes. Therefore, the expert advisor should implement a method to save and retrieve these volatile data. One way to implement this is to use a descendant of the CFileBin class, CExpertFile. The following code snippet shows the declaration of CExpertFileBase, its base class:
class CExpertFileBase : public CFileBin { public: CExpertFileBase(void); ~CExpertFileBase(void); void Handle(const int handle) { m_handle=handle; }; uint WriteBool(const bool value); bool ReadBool(bool &value); };
Here, we are extending CFileBin to explicity declare methods to writing and reading data of Boolean type.
At the end of the class file, we declare an instance of the CExpertFile class. This instance will be used throughout the expert advisor if it were to save and load volatile data. Alternatively, one may simply rely on the Save and Load methods inherited from CObject, and process the saving and loading of data in the usual way. However, this can be a very rigorous process. A great deal of effort and lines of code can be saved from using CFile (or its heirs) alone.
//CExpertFileBase class definition //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "..\..\MQL5\File\ExpertFile.mqh" #else #include "..\..\MQL4\File\ExpertFile.mqh" #endif //+------------------------------------------------------------------+ CExpertFile file; //+------------------------------------------------------------------+
The order manager saves volatile data through its Save method:
bool COrderManagerBase::Save(const int handle) { if(handle==INVALID_HANDLE) return false; file.WriteDouble(m_lotsize); file.WriteString(m_comment); file.WriteInteger(m_expiration); file.WriteInteger(m_history_count); file.WriteInteger(m_max_orders_history); file.WriteBool(m_trade_allowed); file.WriteBool(m_long_allowed); file.WriteBool(m_short_allowed); file.WriteInteger(m_max_orders); file.WriteInteger(m_max_trades); file.WriteObject(GetPointer(m_orders)); file.WriteObject(GetPointer(m_orders_history)); return true; }
Most of these data are of primitive types, except the last two, which are the orders and historical orders containers. For these data, the WriteObject method of CFileBin is used, which simply calls the Save method of the object to be written. The following code shows the Save method of COrderBase:
bool COrderBase::Save(const int handle) { if(handle==INVALID_HANDLE) return false; file.WriteBool(m_initialized); file.WriteBool(m_closed); file.WriteBool(m_suspend); file.WriteInteger(m_magic); file.WriteDouble(m_price); file.WriteLong(m_ticket); file.WriteEnum(m_type); file.WriteDouble(m_volume); file.WriteDouble(m_volume_initial); file.WriteString(m_symbol); file.WriteObject(GetPointer(m_order_stops)); return true; }
As we can see here, the process just repeats when saving objects. For primitive data types, the data is simply saved to file as usual. For complex data types, the Save method of the object is called through CFileBin's WriteObject method.
In cases where multiple instance of CExpertAdvisor is present, the container CExpertAdvisors should also have the capacity to save data:
bool CExpertAdvisorsBase::Save(const int handle) { if(handle!=INVALID_HANDLE) { for(int i=0;i<Total();i++) { CExpertAdvisor *e=At(i); if(!e.Save(handle)) return false; } } return true; }
The method calls the Save method of each CExpertAdvisor instance. The single file handle means that there would only be one save file for each expert advisor file. It is possible for each CExpertAdvisor instance to have its own save file, but this would be the more complicated approach.
The more complex part is the loading of data. In saving data, the values of some class members are simply written to file. On the other hand, when loading data, the object instances would need to be recreated in ideally the same state prior to saving. The following code shows the Load method of the order manager:
bool COrderManagerBase::Load(const int handle) { if(handle==INVALID_HANDLE) return false; if(!file.ReadDouble(m_lotsize)) return false; if(!file.ReadString(m_comment)) return false; if(!file.ReadInteger(m_expiration)) return false; if(!file.ReadInteger(m_history_count)) return false; if(!file.ReadInteger(m_max_orders_history)) return false; if(!file.ReadBool(m_trade_allowed)) return false; if(!file.ReadBool(m_long_allowed)) return false; if(!file.ReadBool(m_short_allowed)) return false; if(!file.ReadInteger(m_max_orders)) return false; if(!file.ReadInteger(m_max_trades)) return false; if(!file.ReadObject(GetPointer(m_orders))) return false; if(!file.ReadObject(GetPointer(m_orders_history))) return false; for(int i=0;i<m_orders.Total();i++) { COrder *order=m_orders.At(i); if(!CheckPointer(order)) continue; COrderStops *orderstops=order.OrderStops(); if(!CheckPointer(orderstops)) continue; for(int j=0;j<orderstops.Total();j++) { COrderStop *orderstop=orderstops.At(j); if(!CheckPointer(orderstop)) continue; for(int k=0;k<m_stops.Total();k++) { CStop *stop=m_stops.At(k); if(!CheckPointer(stop)) continue; orderstop.Order(order); if(StringCompare(orderstop.StopName(),stop.Name())==0) { orderstop.Stop(stop); orderstop.Recreate(); } } } } return true; }
The code above for the COrderManager much more complicated in contrast with CExpertAdvisor's Load method. The reason is that unlike the order manager, the instances of CExpertAdvisor are created during OnInit, and so the container would simply have to call the Load method of each instance of CExpertAdvisor, rather than using the ReadObject method of CFileBin.
Class instances that were not created during OnInit, will have to be created as well when reloading the expert advisor. This is achieved by extending the CreateElement method of CArrayObj. An object cannot simply create itself on its own, so it has to be created by its parent object or container, or even from the main source or header file itself. An example can be seen in the extended CreateElement method found on COrdersBase. Under this class, the container is COrders (a descendant of COrdersBase), and the object to be created is of type COrder:
bool COrdersBase::CreateElement(const int index) { COrder*order=new COrder(); if(!CheckPointer(order)) return(false); order.SetContainer(GetPointer(this)); if(!Reserve(1)) return(false); m_data[index]=order; m_sort_mode=-1; return CheckPointer(m_data[index]); }
Here, aside from creating the element, we also set its parent object or container, in order to differentiate if it belongs to the list of active trades (m_orders class member of COrderManagerBase) or the history (m_orders_history of COrderManagerBase).
Examples
Examples #1-#4 in this article are modified versions of the four examples found on the previous article (see Cross-Platform Expert Advisor: Custom Stops, Trailing, and Breakeven). Let us take a look at the most complex example, expert_custom_trail_ha_ma.mqh, which is a modified version of custom_trail_ha_ma.mqh.
Before the OnInit function, we declared the following global object instances:
COrderManager *order_manager; CSymbolManager *symbol_manager; CSymbolInfo *symbol_info; CSignals *signals; CMoneys *money_manager; CTimes *time_filters;
We replace this with an instance of CExpert. Some of the above can be found within CExpetAdvisor itself (e.g. COrderManager), while the rest have to be instantiated during OnInit (i.e. containers):
CExpertAdvisors experts;
At the very beginning of the method, we create an instance of CExpertAdvisor. We also call its Init method inputting the most basic settings:
int OnInit() { //--- CExpertAdvisor *expert=new CExpertAdvisor(); expert.Init(Symbol(),Period(),12345,true,true,true); //--- other code //--- return(INIT_SUCCEEDED); }
CSymbolInfo / CSymbolManager no longer needs to be instantiated since the instance of the CExpertAdvisor class is able to create instances of these classes on its own.
The user-defined function would also have to be removed, since our new expert advisor will no longer need these.
We removed the global declaration for the containers in our code, so they need to be declared from within OnInit. An example of this is the time filters container (CTimeFilters), as shown in the following code, found within the OnInit function:
CTimes *time_filters=new CTimes();
Pointers to containers that are previously “added” to the order manager are instead added to the instance of CExpertAdvisor. All the other containers that are not added to the order manager will have to be added to the
CExpertAdvisor instance as well. It would be the instance of COrderManager that would store the pointers. The CExpertAdvisor instance only creates wrapper methods.
After this, we add the CExpertAdvisor instance to an instance of CExpertAdvisors. We then call the InitComponents method of the CExpertAdvisors instance. This would ensure the initialization of all instances of CExpertAdvisor and their components.
int OnInit() { //--- //--- other code experts.Add(GetPointer(expert)); if(!experts.InitComponents()) return(INIT_FAILED); //--- other code //--- return(INIT_SUCCEEDED); }
Finally, we insert the code needed for loading if the expert advisor was interrupted in its operation:
int OnInit() { //--- //--- other code file.Open(savefile,FILE_READ); if(!experts.Load(file.Handle())) return(INIT_FAILED); file.Close(); //--- return(INIT_SUCCEEDED); }
If the expert advisor fails to load from the file, it would return INIT_FAILED. However, in the event where no save file was supplied (and hence would generate INVALID_HANDLE), the expert advisor will not fail initialization, since the Load methods of CExpertAdvisors and CExpertAdvisor both returns true upon receiving an invalid handle. There is some risk with this approach, but it is very unlikely for a save file to be opened by another program. Just make sure that each instance of expert advisor running on a chart has an exclusive save file (just like magic number).
The fifth example cannot be found on the previous article. Rather, it combines all the four expert advisors in this article into a single expert advisor. It simply uses a slightly modified version of the OnInit function of each of the expert advisors and declare it as a custom-defined function. Its return value is of type CExpertAdvisor*. If the creation of the expert advisor failed, it would return NULL instead of INIT_SUCCEEDED. The following code shows the updated OnInit function of the combined expert advisor header file:
int OnInit() { //--- CExpertAdvisor *expert1=expert_breakeven_ha_ma(); CExpertAdvisor *expert2=expert_trail_ha_ma(); CExpertAdvisor *expert3=expert_custom_stop_ha_ma(); CExpertAdvisor *expert4=expert_custom_trail_ha_ma(); if (!CheckPointer(expert1)) return INIT_FAILED; if (!CheckPointer(expert2)) return INIT_FAILED; if (!CheckPointer(expert3)) return INIT_FAILED; if (!CheckPointer(expert4)) return INIT_FAILED; experts.Add(GetPointer(expert1)); experts.Add(GetPointer(expert2)); experts.Add(GetPointer(expert3)); experts.Add(GetPointer(expert4)); if(!experts.InitComponents()) return(INIT_FAILED); file.Open(savefile,FILE_READ); if(!experts.Load(file.Handle())) return(INIT_FAILED); file.Close(); //--- return(INIT_SUCCEEDED); }
The expert advisor starts by instantiating each instance of CExpertAdvisor. It would then proceed to checking each of the pointers to CExpertAdvisor. If the pointer is not dynamic, then the function returns INIT_FAILED, and the initialization fails. If each of the instances passes the checking of pointers, these pointers are then stored in an instance of CExpertAdvisors. The CExpertAdvisors instance (the container, not the expert advisor instance) would then initialize its components and load previous data if necessary.
The expert advisor uses custom-defined functions to create an instance of CExpertAdvisor. The following code shows the function used to create the 4th expert advisor instance:
CExpertAdvisor *expert_custom_trail_ha_ma() { CExpertAdvisor *expert=new CExpertAdvisor(); expert.Init(Symbol(),Period(),magic4,true,true,true); CMoneys *money_manager=new CMoneys(); CMoney *money_fixed=new CMoneyFixedLot(0.05); CMoney *money_ff=new CMoneyFixedFractional(5); CMoney *money_ratio=new CMoneyFixedRatio(0,0.1,1000); CMoney *money_riskperpoint=new CMoneyFixedRiskPerPoint(0.1); CMoney *money_risk=new CMoneyFixedRisk(100); money_manager.Add(money_fixed); money_manager.Add(money_ff); money_manager.Add(money_ratio); money_manager.Add(money_riskperpoint); money_manager.Add(money_risk); expert.AddMoneys(GetPointer(money_manager)); CTimes *time_filters=new CTimes(); if(time_range_enabled && time_range_end>0 && time_range_end>time_range_start) { CTimeRange *timerange=new CTimeRange(time_range_start,time_range_end); time_filters.Add(GetPointer(timerange)); } if(time_days_enabled) { CTimeDays *timedays=new CTimeDays(sunday_enabled,monday_enabled,tuesday_enabled,wednesday_enabled,thursday_enabled,friday_enabled,saturday_enabled); time_filters.Add(GetPointer(timedays)); } if(timer_enabled) { CTimer *timer=new CTimer(timer_minutes*60); timer.TimeStart(TimeCurrent()); time_filters.Add(GetPointer(timer)); } switch(time_intraday_set) { case INTRADAY_SET_1: { CTimeFilter *timefilter=new CTimeFilter(time_intraday_gmt,intraday1_hour_start,intraday1_hour_end,intraday1_minute_start,intraday1_minute_end); time_filters.Add(timefilter); break; } case INTRADAY_SET_2: { CTimeFilter *timefilter=new CTimeFilter(0,0,0); timefilter.Reverse(true); CTimeFilter *sub1 = new CTimeFilter(time_intraday_gmt,intraday2_hour1_start,intraday2_hour1_end,intraday2_minute1_start,intraday2_minute1_end); CTimeFilter *sub2 = new CTimeFilter(time_intraday_gmt,intraday2_hour2_start,intraday2_hour2_end,intraday2_minute2_start,intraday2_minute2_end); timefilter.AddFilter(sub1); timefilter.AddFilter(sub2); time_filters.Add(timefilter); break; } default: break; } expert.AddTimes(GetPointer(time_filters)); CStops *stops=new CStops(); CCustomStop *main=new CCustomStop("main"); main.StopType(stop_type_main); main.VolumeType(VOLUME_TYPE_PERCENT_TOTAL); main.Main(true); //main.StopLoss(stop_loss); //main.TakeProfit(take_profit); stops.Add(GetPointer(main)); CTrails *trails=new CTrails(); CCustomTrail *trail=new CCustomTrail(); trails.Add(trail); main.Add(trails); expert.AddStops(GetPointer(stops)); MqlParam params[1]; params[0].type=TYPE_STRING; #ifdef __MQL5__ params[0].string_value="Examples\\Heiken_Ashi"; #else params[0].string_value="Heiken Ashi"; #endif SignalHA *signal_ha=new SignalHA(Symbol(),0,1,params,signal_bar); SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,0,mamethod,maapplied,signal_bar); CSignals *signals=new CSignals(); signals.Add(GetPointer(signal_ha)); signals.Add(GetPointer(signal_ma)); expert.AddSignal(GetPointer(signals)); //--- return expert; }
As we can see, the code looks very similar to the OnInit function of the original expert advisor header file (expert_custom_trail_ha_ma.mqh). The other custom-defined functions are also organized in the same way.
Final Notes
Before concluding this article, any reader who wishes to use this library should be made aware of these factors that contribute to the library's development:
At the time of this writing, the library featured in this article has over 10,000 lines of code (including comments). Despite this, it still remains a work in progress. More work needs to be done in order to fully utilize the capabilities of both MQL4 and MQL5.
The author started working on this project prior to the introduction of hedging mode in MetaTrader 5. This has greatly influenced the further development of the library. As a result, the library tends to be more closer to adopting the conventions used in MetaTrader 4 than MetaTrader 5. Furthermore, the author also experienced some compatibility issues with some build updates released over the last few years, which led to some minor and major adjustments to the code (and some delay on publishing some articles). At the time of this writing, the author experienced the build updates for both platforms to be less frequent and more stable over time. This trend is expected to further improve. Nevertheless, future build updates that may cause incompatibilities would still need to be addressed.
The library relies on data saved on memory to keep track of its own trades. This causes the expert advisors created using this library to heavily depend on saving and loading data in order to deal with possible interruptions the expert advisor may experience during its execution. Future work on this library, as well as any other library aiming at cross-platform compatibility, should be geared at achieving a stateless or near-stateless implementation, similar to the implementation of the MQL5 Standard Library.
As a final remark, the library featured in this article should not be viewed as a permanent solution. Rather, it should be used as an opportunity for a smoother transition from MetaTrader 4 to MetaTrader 5. The incompatibilities between MQL4 and MQL5 presents a huge roadblock to traders who intend to transition to the new platform. As a result, the MQL4 source code of their expert advisors need to be refactored in order to be made compatible with the MQL5 compiler. The library featured in this article is provided as a means to deploy an expert advisor to the new platform with little or no adjustments to the main expert advisor source code. This can help the trader in his decision whether to still use MetaTrader 4, or switch to MetaTrader 5. In the event that he decided to switch, very little adjustments would be necessary, and the trader can operate the usual way with his expert advisors. On the other hand, if he decides to stay on using the old platform, he is provided an option to quickly switch to the new platform once MetaTrader 4 becomes legacy software.
Conclusion
This article has featured the CExpertAdvisor and CExpertAdvisors class objects, which are used to integrate all the components of a cross-platform expert advisor discussed in this article series. The article discusses how the two classes are instantiated and linked with the other components of a cross-platform expert advisor. It also introduces some solutions to problems usually encountered by expert advisors, such as new bar detection and the saving and loading of volatile data.
Programs Used in the Article
# | Name |
Type |
Description |
---|---|---|---|
1. |
expert_breakeven_ha_ma.mqh |
Header File |
The main header file used in the first example |
2. |
expert_breakeven_ha_ma.mq4 | Expert Advisor |
The main source file used for the MQL4 version in the first example |
3. |
expert_breakeven_ha_ma.mq5 | Expert Advisor | The main source file used for the MQL5 version in the first example |
4. |
expert_trail_ha_ma.mqh | Header File | The main header file used in the second example |
5. |
expert_trail_ha_ma.mq4 | Expert Advisor | The main source file used for the MQL4 version in the second example |
6. |
expert_trail_ha_ma.mq5 | Expert Advisor | The main source file used for the MQL5 version in the second example |
7. |
expert_custom_stop_ha_ma.mqh | Header File | The main header file used in the third example |
8. |
expert_custom_stop_ha_ma.mq4 | Expert Advisor | The main source file used for the MQL4 version in the third example |
9. |
expert_custom_stop_ha_ma.mq5 | Expert Advisor | The main source file used for the MQL5 version in the third example |
10. |
expert_custom_trail_ha_ma.mqh | Header File | The main header file used in the fourth example |
11. |
expert_custom_trail_ha_ma.mq4 | Expert Advisor | The main source file used for the MQL4 version in the fourth example |
12. |
expert_custom_trail_ha_ma.mq5 | Expert Advisor | The main source file used for the MQL5 version in the fourth example |
13. |
combined.mqh | Header File | The main header file used in the fifth example |
14. |
combined.mq4 | Expert Advisor | The main source file used for the MQL4 version in the fifth example |
15. |
combined.mq5 | Expert Advisor | The main source file used for the MQL5 version in the fifth example |
Class Files Featured in the Article
# |
Name |
Type |
Description |
---|---|---|---|
1. | MQLx\Base\Expert\ExperAdvisorsBase | Header File |
CExpertAdvisors (CExpertAdvisor container, base class) |
2. |
MQLx\MQL4\Expert\ExperAdvisors | Header File | CExpertAdvisors (MQL4 version) |
3. |
MQLx\MQL5\Expert\ExperAdvisors | Header File |
CExpertAdvisors (MQL5 version) |
4. |
MQLx\Base\Expert\ExperAdvisorBase | Header File |
CExpertAdvisor (base class) |
5. |
MQLx\MQL4\Expert\ExperAdvisor | Header File |
CExpertAdvisor (MQL4 version) |
6. |
MQLx\MQL5\Expert\ExperAdvisor | Header File |
CExpertAdvisor (MQL5 version) |
7. |
MQLx\Base\Candle\CandleManagerBase | Header File | CCandleManager (CCandle container, base class) |
8. |
MQLx\MQL4\Candle\CandleManager | Header File | CCandleManager (MQL4 version) |
9. |
MQLx\MQL5\Candle\CandleManager | Header File | CCandleManager (MQL5 version) |
10. |
MQLx\Base\Candle\CandleBase | Header File | CCandle (base class) |
11. |
MQLx\MQL4\Candle\Candle | Header File | CCandle (MQL4 version) |
12. |
MQLx\MQL5\Candle\Candle | Header File |
CCandle (MQL5 version) |
13. |
MQLx\Base\File\ExpertFileBase | Header File | CExpertFile(base class) |
14. |
MQLx\MQL4\File\ExpertFile | Header File | CExpertFile(MQL4 version) |
15. |
MQLx\MQL5\File\ExpertFile | Header File | CExpertFile(MQL5 version) |





- Free trading apps
- Free Forex VPS for 24 hours
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Another question. If I want to have multi pair traded expert how do I initialize it? In CExpertAdvisorBase::Init() I have to specify a symbol name.
How do I create a MM type based on previous orders results? It's so unflexible...
I managed to create a custom MM class, thanks.
How do I get the order close reason? How do I know it's closed due stop (any virtual or broker)?
How to use CEventAggregator class? Do you have any examples?
There is error. I commented the wrong value.
I see. Thank you for this. It is now corrected. You can see the latest version here:
https://github.com/iceron/MQLx/commits/master