![Cross-Platform Expert Advisor: Orders](https://c.mql5.com/i/articles/overlay.png)
Cross-Platform Expert Advisor: Orders
Table of Contents
- Introduction
- Conventions
- Trade Identifier (Ticket)
- States
- Volume
- Orders Container
- Example
- Extensions
- Conclusion
Introduction
MetaTrader 4 and MetaTrader 5 uses different conventions in processing trade requests. This article discusses the possibility of using a class object that can be used to represent the trades processed by the server, in order for a cross-platform expert advisor to further work on them, regardless of the version of the trading platform and mode being used.
Conventions
There are many differences on how MetaTrader 4 and MetaTrader 5 process trade requests from the terminal. In saving the details of trade requests processed by the trade servers, we need to consider three different versions/modes in both trading platforms: (1) MetaTrader 4, (2) MetaTrader 5, netting mode, and (3) MetaTrader 5, hedging mode.
MetaTrader 4
In MetaTrader 4, when an expert advisor successfully sends an order, it receives a ticket number, which the a numerical identifier for that order. When modifying or closing the order, the same ticket is normally used until it leaves the market.
The situation is a bit more complicated when partially closing an order. The operation is done by also using the OrderClose function, by specifying the number of lots less than the total lot size of the order in question. When such a trade request is sent, the order (or order ticket) is closed with by a certain lot size amount indicated in the function call. The remaining lot size would then remain on the market as a new order with the same type as the partially closed order. Since the OrderClose function only returns a Boolean variable, there is no quick way to get the new ticket other than re-examining the list of orders currently active on the account. Note that it is not possible to get the ticket using the OrderClose function alone. The function returns a Boolean variable, whereas functions like OrderSend, in MQL4, returns a valid ticket upon a successful transaction.
MetaTrader 5 (Netting)
The process of trade operations in MetaTrader 5 will look quite complicated at first glance, but is much simpler to manage than in MetaTrader 4. This would be true at least for the trader's (non-programmer) side.
The default mode in MetaTrader 5 is netting mode. Under this mode, the results of orders processed by the server are consolidated into a single position. The volume of type of this particular position can change over time, based on the volume and types of orders entered. On the programmer's side, it is a little complicated. Unlike in MQL4, where there is only a concept of orders, the programmer will have to deal with three different types of tokens used in trading. The following table shows some comparison between the netting mode in MQL5 and its rough equivalent in MQL4:
Artifact | MQL5 (Netting) | MQL4 (Rough Equivalent) |
---|---|---|
Order | Trade request (pending or market) | Trade request (pending or market) |
Deal | Deal(s) made based on a single order (market order, or executed pending order) | Single market order as reflected on trading terminal |
Position | Trades (consolidated) | Sum of all market orders on trading terminal (order types apply) |
In MQL5, orders, once executed, are unchangeable on the client-side, whereas in MQL4, some properties can still be changed while an order is in the market. That is, in the former, an order is simply a trade request sent to the server. On the latter, it can be used to represent the trade request as well as the result of such a request. In this aspect, MQL5 made the entire process more complex in order to make it less ambiguous, since an apparent distinction is made between a trade request and a trade result. An order in MQL4 can enter the market and leave it with a different configuration, whereas in MQL5 all trades can be traced back to the orders or trade requests that triggered them.
When one sends a trade request, there are only two outcomes: processed or not processed. If the trade was not processed, it means that there was no deal, as the trade server was not able to process it for some reason (usually due to errors). Now, if the trade is processed, in MQL5, the client and the server have a deal. In this case, the order can be fully executed or partially executed.
MetaTrader 4 does not have this option, as the order is only executed entirely or not (fill or kill).
One notable disadvantage with this mode in MetaTrader 5 is that it does not allow hedging. The type of position on a given symbol can change. For example, if there is a 0.1 lot long position on a given symbol, entering a sell order with a volume of 1.0 will convert the position for that symbol into a short position, with a volume of 0.9 lot.
MetaTrader 5 (Hedging)
The hedging mode on MetaTrader 5 resemble the conventions used in MetaTrader 4. Rather than consolidating all the processed trades into a single position, hedging mode allows a symbol to have more than one position. A position is generated whenever a pending order has triggered or a trade request by market has been processed by the trade server.
Artifact | MQL5 (Netting) | MQL4 (Rough Equivalent) |
---|---|---|
Order | Trade request (pending or market) | Trade request (pending or market) |
Deals | Deal(s) made based on a single order | Market orders as reflected on trading terminal |
Position | Trades (consolidated) based on a single trade request | Order as reflected on trading terminal |
In order to make a cross-platform expert advisor able to accommodate these differences, a possible solution would be to have the expert advisor store the details of the individual trades it has placed on the market. Every time a trade has been successfully placed, a copy of the details of the order will be saved on a class object, the COrder class. The following code shows the declaration of its base class:
class COrderBase : public CObject { protected: bool m_closed; bool m_suspend; long m_order_flags; int m_magic; double m_price; ulong m_ticket; ENUM_ORDER_TYPE m_type; double m_volume; double m_volume_initial; string m_symbol; public: COrderBase(void); ~COrderBase(void); //--- getters and setters void IsClosed(const bool); bool IsClosed(void) const; void IsSuspended(const bool); bool IsSuspended(void) const; void Magic(const int); int Magic(void) const; void Price(const double); double Price(void) const; void OrderType(const ENUM_ORDER_TYPE); ENUM_ORDER_TYPE OrderType(void) const; void Symbol(const string); string Symbol(void) const; void Ticket(const ulong); ulong Ticket(void) const; void Volume(const double); double Volume(void) const; void VolumeInitial(const double); double VolumeInitial(void) const; //--- output virtual string OrderTypeToString(void) const; //--- static methods static bool IsOrderTypeLong(const ENUM_ORDER_TYPE); static bool IsOrderTypeShort(const ENUM_ORDER_TYPE); };
Since the EA remembers its own trades, it can work with greater independence on the conventions used by the trading platform it is being run on. Its disadvantage, however, lies in the fact that the instances of this class will persist only during the expert advisor's operation. In case the expert advisor or trading platform would need to be restarted, all the saved data would be loss unless there is a mean to save and load the information.
Trade Identifier (Ticket)
Another possible hindrance in using instances of COrder in creating cross-platform compatible expert advisors is on how the order (or position) ticket numbers should be stored. The differences are summarized in the following table:
Operation | MQL4 | MQL5 (Netting) | MQL5 (Hedging) |
---|---|---|---|
Sending an Order | New Order Ticket | New Position Ticket (no existing positions) or Existing Position Ticket (with existing position) | New Position Ticket |
Partial Close | New Order Ticket | Same ticket (if with remainder), otherwise N/A | Same Ticket |
When sending an order, all the three versions have different ways to represent the trade entered. In MQL4, when a trade request was successful, a new order will be opened. This new order will be represented by an identifier (order ticket). In MQL5 netting mode, each trade request to enter a trade is represented by an order ticket. However, an order ticket may not be the best way to represent the trade entered, but the resulting position itself. The reason is that unlike in MQL4, the order ticket cannot be used directly when further working on the resulting trade it has entered on the market (but getting the ticket number may be useful when trying to get the position that resulted from executing a certain order). Furthermore, when there is an existing position of the same type, the position ticket will remain the same (unlike in MQL4). On the other hand, in MQL5 hedging mode, each new deal generates a new position (a rough equivalent of an MQL4 order ticket). However, its difference in MQL4 is that, a single trade request always results in a single order, whereas in MQL5 (hedging mode), it is possible for a single order to have more than one deals (when the filling policy is set other than SYMBOL_FILLING_FOK).
There is also another issue when partially closing a market order (MQL4) or position (MQL5). As stated earlier, in MQL4, when a particular ticket is closed at a volume less than its total (OrderLots), the ticket representing the trade is closed, while the remaining volume will be assigned a new ticket with the same type as the one partially closed. This is a little different in MQL5. In netting mode, it requires a trade on the opposite direction (buy on sell, or sell on buy), in order to close a position (either partially or in full). In hedging mode, the process is more similar to MQL4 (OrderClose versus CTrade's PositionClose), but unlike in MQL4, partially closing a position will not trigger any change on the identifier representing it.
One way to solve this problem is to
split the implementation on how to represent the identifier for a
particular trade on two platforms. Since the an order ticket does not
change in MetaTrader 5, we can simply assign it a typical numerical
variable. On the other hand, for the MetaTrader 4 version, we will
use an instance of CArrayInt to store the ticket numbers. For COrderBase (and consequently for the MQL5 COrder version), the following code for the Ticket method will be used:
COrderBase::Ticket(const ulong value) { m_ticket=value; }
This method will be overridden on the MQL4 version, with the following code:
COrder::Ticket(const ulong ticket) { m_ticket_current.InsertSort((int)ticket); }
States
Within the internal states of orders on the cross-platform expert advisor, there would be at least two possible states:
Closed
Suspend
The two states are very similar but there is a fundamental difference. The closed state of an order indicates that an order is already closed and the expert advisor would need to archive the order in its internal data. This would be roughly equivalent to moving an order to the history in MQL4. The suspend state, on the other hand, would only happen when the expert advisor failed to close an order or one of the stops linked to it. In this case, the expert advisor can attempt to close the order again (and its stops) until its it is fully closed.
Volume
In MQL4, the calculation of volume is straightforward. Whenever an expert advisor sends a trade request, the volume of the request is also included, and it would be either denied or accepted. This is equivalent to the Fill or Kill margin policy in MQL5, which is also the default setting for the trade object (CTrade and CExpertTrade). Getting the common feature would give us the FOK margin policy. And in order to make the handling of volume consistent between MQL4 and MQL5, one approach would be to derive the volume of the instance of COrder based on the volume of the trade request itself. However, this would mean that for the MQL5 version, we would need to stick to the FOK policy. It is still possible to use the other margin policies, but the results would be slightly different (i.e. the count of COrder instances on the MQL5 version on a given test of the same EA may be greater).
Orders Container
If the expert advisor is to handle more than one instance of COrder, some method of organization may be needed. One of the classes that will facilitate this is the orders container, or COrders. The class extends CArrayObj and stores instances of COrder. This would allow for easy storage and retrieval of trades entered by the expert advisor. The base template for the said class is shown below:
#include <Arrays\ArrayObj.mqh> #include "OrderBase.mqh" class CExpertAdvisor; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class COrdersBase : public CArrayObj { public: COrdersBase(void); ~COrdersBase(void); virtual bool NewOrder(const ulong,const string,const int,const ENUM_ORDER_TYPE,const double,const double); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ COrdersBase::COrdersBase(void) { if(!IsSorted()) Sort(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ COrdersBase::~COrdersBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price) { COrder *order=new COrder(ticket,symbol,type,volume,price); if(CheckPointer(order)==POINTER_DYNAMIC) if(InsertSort(GetPointer(order))) order.Magic(magic); return false; } //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "..\..\MQL5\Order\Orders.mqh" #else #include "..\..\MQL4\Order\Orders.mqh" #endif //+------------------------------------------------------------------+
Since its main function is to store instances of COrder, it has to have a means to add instances of the said class. The default method Add of CArrayObj may not always be ideal, as an instance of COrder requires to be instantiated. For this, we would have the NewOrder method, which would already create a new instance of COrder and automatically add it as an array member:
bool COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price) { COrder *order=new COrder(ticket,symbol,type,volume,price); if(CheckPointer(order)==POINTER_DYNAMIC) if(InsertSort(GetPointer(order))) order.Magic(magic); return false; }
Now that the base template is done, other methods may be added to this class. One example is an OnTick method. In this method, the container class would simply iterate over the items it has stored (COrder). Another possibility is to have the class COrder have an OnTick method as well. Then, it can then be coded such that this method on COrders is called every tick.
Example
Our example code tries to enter a long position. After the position has been entered, the details of the trade are then stored on an instance of COrder. This is achieved by calling the NewOrder method of COrders (which would deal with the creation of an instance of COrder).
Both versions will use instances of a trade object (CExpertTradeX), an orders object (COrders), and symbol object (CSymbolInfo). On the OnTick handler of the expert advisors, the trade object will try to enter a long position using its Buy method. The only difference between the two versions (MQL4 and MQL5) is on how the details of the trade is to be retrieved. For the MQL5 version, the details are retrieved by using HistoryOrderSelect and other related functions. The order ticket is retrieved using the ResultOrder method of the trade object. The implementation for this version is shown below:
ulong retcode=trade.ResultRetcode(); ulong order = trade.ResultOrder(); if(retcode==TRADE_RETCODE_DONE) { if(HistoryOrderSelect(order)) { ulong ticket=HistoryOrderGetInteger(order,ORDER_TICKET); ulong magic=HistoryOrderGetInteger(order,ORDER_MAGIC); string symbol = HistoryOrderGetString(order,ORDER_SYMBOL); double volume = HistoryOrderGetDouble(order,ORDER_VOLUME_INITIAL); double price=HistoryOrderGetDouble(order,ORDER_PRICE_OPEN); ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)HistoryOrderGetInteger(order,ORDER_TYPE); orders.NewOrder((int)ticket,symbol,(int)magic,order_type,volume,price); } }
The trade object in MQL4 has less features than in the MQL5 version. One may extend the trade object for this version, or simply iterate over all the active orders on the account in order to get the trade just entered:
for(int i=0;i<OrdersTotal();i++) { if(!OrderSelect(i,SELECT_BY_POS)) continue; if(OrderMagicNumber()==12345) orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)OrderType(),OrderLots(),OrderOpenPrice()); }
The full code for the main header file is shown below:
(test_orders.mqh)
#include <MQLx-Orders\Base\Trade\ExpertTradeXBase.mqh> #include <MQLx-Orders\Base\Order\OrdersBase.mqh> CExpertTradeX trade; COrders orders; CSymbolInfo symbolinfo; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- if(!symbolinfo.Name(Symbol())) { Print("failed to initialize symbol"); return INIT_FAILED; } trade.SetSymbol(GetPointer(symbolinfo)); trade.SetExpertMagicNumber(12345); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(!symbolinfo.RefreshRates()) { Print("cannot refresh symbol"); return; } if(trade.Buy(1.0,symbolinfo.Ask(),0,0)) { #ifdef __MQL5__ int retcode=trade.ResultRetCode(); ulong order = trade.ResultOrder(); if(retcode==TRADE_RETCODE_DONE) { if(HistoryOrderSelect(order)) { ulong ticket=HistoryOrderGetInteger(order,ORDER_TICKET);; ulong magic=HistoryOrderGetInteger(order,ORDER_MAGIC); string symbol = HistoryOrderGetString(order,ORDER_SYMBOL); double volume = HistoryOrderGetDouble(order,ORDER_VOLUME_INITIAL); double price=HistoryOrderGetDouble(order,ORDER_PRICE_OPEN); ENUM_ORDER_TYPE order_type=order_type; m_orders.NewOrder((int)ticket,symbol,(int)magic,order_type,volume,price); } } #else for(int i=0;i<OrdersTotal();i++) { if(!OrderSelect(i,SELECT_BY_POS)) continue; if(OrderMagicNumber()==12345) orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)OrderType(),OrderLots(),OrderOpenPrice()); } #endif } Sleep(5000); ExpertRemove(); } //+------------------------------------------------------------------+
The header file contains all of the needed code. Thus, the main source files will only need to have at least the pre-processor directive to include test_orders.mqh:
(test_orders.mq4 and test_orders.mq5)
#include "test_orders.mqh"
Running the expert advisor on the platforms will give the following log entries:
In MetaTrader 4, the following log file will be generated:
Expert test_orders EURUSD,H1: loaded successfully
test_orders EURUSD,H1: initialized
test_orders EURUSD,H1: open #358063536 buy 1.00 EURUSD at 1.12470 ok
test_orders EURUSD,H1: ExpertRemove function called
test_orders EURUSD,H1: uninit reason 0
Expert test_orders EURUSD,H1: removed
The following shows a screen shot of the platform upon executing the EA. Note that since the EA calls the ExpertRemove function, it is automatically removed from the chart as soon as it executes its code (only single execution for the OnTick handler).
In MetaTrader 5, the log file generated will be almost the same:
Experts expert test_orders (EURUSD,M1) loaded successfully
Trades '3681006': instant buy 1.00 EURUSD at 1.10669 (deviation: 10)
Trades '3681006': accepted instant buy 1.00 EURUSD at 1.10669 (deviation: 10)
Trades '3681006': deal #75334196 buy 1.00 EURUSD at 1.10669 done (based on order #90114599)
Trades '3681006': order #90114599 buy 1.00 / 1.00 EURUSD at 1.10669 done in 275 ms
Experts expert test_orders (EURUSD,M1) removed
Unlike in the MetaTrader 4, the log messages displayed above are found on the Journal tab of the Terminal window (not on the Experts tab):
The EA also prints a message on the Experts tab. However, the message is not about the execution of the trade, but only a message showing that a call to the ExpertRemove function has been made, in comparison with MetaTrader 4, which displays the messages on the Experts tab:
Extensions
Our current implementation lacks certain features, which are often used in real-world expert advisors, such as the following:
1. Initial stoploss and takeprofit values for trades entered
2. Modification of SL and TP levels (e.g. breakeven, trailingstop, or any custom method)
3. Persistence of data - there are differences on how the two platforms save trades and their stop levels. Our class object reconciles the methods of the two, but are saved only on memory. Thus, we would need a method to make the data persistent. That is, we need a method for our expert advisors to save and load the orders information in events such as terminal restarts, or when switching charts in MetaTrader with the cross-platform expert advisor still loaded on that chart.
These will be covered in future articles.
Conclusion
In this article, we have discussed one method by which a cross-platform expert advisor would be able to save the details of a trade request successfully processed by the trade server as an instance of a class object. This object instance can then be used by the expert advisor to further work on the trade it represents, based on a particular strategy. A basic template has been provided for this class object, which can be further developed in order to be useful in more sophisticated trading strategies.
![MQL5 vs QLUA - Why trading operations in MQL5 are up to 28 times faster?](https://c.mql5.com/2/24/speed_over_28_03.png)
![MQL5 Cookbook - Trading signals of moving channels](https://c.mql5.com/2/24/ava2.png)
![LifeHack for trader: "Quiet" optimization or Plotting trade distributions](https://c.mql5.com/2/24/avaf2i.png)
![How to quickly develop and debug a trading strategy in MetaTrader 5](https://c.mql5.com/2/24/avae17.png)
![MQL5 - Language of trade strategies built-in the MetaTrader 5 client terminal](https://c.mql5.com/i/registerlandings/logo-2.png)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Enrico is it possible to check if the order (or position) was partially closed.
In MT4 I have a new ticket in case of partial close. In fact, it is not a new entry but still previous entry. So I need separate such orders. The ones which indicate a true market entry and the ones which appear after partial close (not a new entry). Is it possible using your classes?
Or it would be one COrder object until the order closed fully? I mean after partial close no new COrder object?
If so how do I get the total order profit?
Also, how do I get initial order ticket or initial order type?
Suppose I have sell 5.00 #1 order at 1.09246. Next partial close buy 2.00 #2 at 1.08896. And final close remaining buy 3.00 #3 at 1.09161 (due to trailing stop actually).
This is my code:
To simplify let's call all these 3 orders as position.
IsClosed() method returns true when the position closes (at last order #3). But how do I get the position type, position first order ticket, position open price. Position initial volume looks like can be calculated by using InitVolume() method. What about the rest?
Suppose I have sell 5.00 #1 order at 1.09246. Next partial close buy 2.00 #2 at 1.08896. And final close remaining buy 3.00 #3 at 1.09161 (due to trailing stop actually).
This is my code:
To simplify let's call all these 3 orders as position.
IsClosed() method returns true when the position closes (at last order #3). But how do I get the position type, position first order ticket, position open price. Position initial volume looks like can be calculated by using InitVolume() method. What about the rest?
Not entirely sure at what you are trying to do, but to get the unrealized profit/loss in MQL5 on a COrder instance, one way is to get the remaining volume, and then multiply it with the difference between the current market price (bid or ask) and the COrder entry price, and then multiply it with the tick value. The trickier part is when the symbol's point is not equal to the tick size (you will need to divide the difference by the tick size). Gold used to be like this, but not anymore as far as I know. It is simply better to find a broker that offers greater liquidity. The same COrder instance is used until the end. In MQL4, you just need to select the order ticket and call the OrderProfit() function.
When one sends a trade request, there are only two outcomes: processed or not processed. If the trade was not processed, it means that there was no deal, as the trade server was not able to process it for some reason (usually due to errors). Now, if the trade is processed, in MQL5, the client and the server have a deal. In this case, the order can be fully executed or partially executed.
That's not totally exact, you can also have a timeout. Which means you don't know if the order was processed or not. Of course in the end, an order is either processed or not, but it's important to know and process timeout on a live account.
MetaTrader 4 does not have this option, as the order is only executed entirely or not (fill or kill).
...
In MQL4, the calculation of volume is straightforward. Whenever an expert advisor sends a trade request, the volume of the request is also included, and it would be either denied or accepted.
That's not exact. MT4 can also have partially filled orders. Of course it should be rare on Forex which is mainly traded by MT4, but it may happen technically.
In general I don't see the usefulness of the classes your provided in this article, as you still need specific MT4/MT5 in the main code, and compiler directives. (Maybe it's addressed on further articles, I didn't read them yet).