Developing a multi-currency Expert Advisor (Part 3): Architecture revision
Introduction
In the previous articles, we started developing a multi-currency EA that works simultaneously with various trading strategies. The solution provided in the second article is already significantly different from the one presented in the first one. This indicates that we are still in search of the best options.
Let's try to look at the developed system as a whole, abstracting from the small details of the implementation, in order to understand ways to improve it. To do this, let us trace the albeit short, but still noticeable evolution of the system.
First operation mode
We have allocated an EA object (of the CAdvisor class or its descendants), which is an aggregator of trading strategy objects (CStrategy class or its descendants). At the beginning of the EA operation, the following happens in the OnInit() handler:
- EA object is created.
- Objects of trading strategies are created and added to the EA in its array for trading strategies.
The following happens in the OnTick() event handler:
- The CAdvisor::Tick() method is called for the EA object.
- This method iterates through all strategies and calls their CStrategy::Tick() method.
- The strategies within CStrategy::Tick() perform all necessary operations to open and close market positions.
This can be represented schematically like this:
Fig. 1. Operation mode from the first article
The advantage of this mode was that, it was possible to make the EA work with other instances of trading strategies via a number of relatively simple operations provided that you had the source code of an EA following a certain trading strategy.
However, the main drawback quickly emerged: when combining several strategies, we have to reduce the size of the positions opened by each instance of the strategy to one degree or another. This may lead to the complete exclusion of some or even all strategy instances from trading. The more strategy instances we include in parallel work or the smaller the initial deposit, the more likely such an outcome is, since the minimum size of open market positions is fixed.
Also, when several strategy instances were working together, a situation was encountered when opposite positions of the same size were opened. In terms of total volume, this is equivalent to the absence of open positions, but swaps continued to accrue on opposing open positions.
Second operation mode
To eliminate the shortcomings, we decided to move all operations with market positions to a separate place, removing the ability of trading strategies to directly open market positions. This somewhat complicates the reworking of ready-made strategies, but this is not a very big loss, since it is compensated by eliminating the main drawback of the first mode.
Two new entities appear in our operation mode: virtual positions (CVirtualOrder class) and a recipient of trading volumes from strategies (CReceiver class and its descendants).
At the beginning of the EA operation, the following happens in the OnInit() handler:
- A recipient object is created.
- An EA object is created and the created recipient is passed to it.
- Objects of trading strategies are created and added to the EA in its array for trading strategies.
- Each strategy creates its own array of virtual position objects with the required number of these objects.
The following happens in the OnTick() event handler:
- The CAdvisor::Tick() method is called for the EA object.
- This method iterates through all strategies and calls their CStrategy::Tick() method.
- The strategies within CStrategy::Tick() perform all necessary operations to open and close virtual positions. If some event occurs related to a change in the composition of open virtual positions, the strategy remembers that changes by setting a flag.
- If at least one strategy has set a change flag, then the recipient launches the method for adjusting open volumes of market positions. If the adjustment is successful, the change flag for all strategies is reset.
This can be represented schematically like this:
Fig. 2. Operation mode from the second article
In this operation mode, we will no longer be faced with the fact that some strategy instance does not in any way affect the size of open market positions. On the contrary, even an instance that opens a very small virtual volume can become that very drop that overwhelms the total volume of virtual positions from several strategy instances beyond the minimum allowable volume of a market position. The real market position will be opened in this case.
Along the way, we received other pleasant changes, including some possible savings on swaps, less load on the deposit, less observed drawdown and improved trading quality assessment (Sharpe ratio, profit factor).
While testing the second mode, I realized the following:
- Each strategy first handles already open virtual positions to determine the triggered StopLoss and TakeProfit levels. If any of the levels has been reached, then such a virtual position is closed. Therefore, this handling was immediately relocated to the CVirtualOrder class method. But this solution still seems to be an insufficient generalization.
- We have expanded the composition of the base classes by adding new required entities. If we do not want to handle virtual positions, we can still use such base classes, simply passing "empty" objects to them. For example, we can create the CReceiver class object, which contains only empty stub methods. But this also seems more like a temporary solution that needs to be reworked.
- We have added new methods and the property for base class tracking changes in the composition of open virtual positions to the CStrategy base class, which spilled over into the use of these methods in the CAdvisor base class. Again, this looks like a step towards narrowing the possibilities and imposing an overly specific implementation in the base class.
- The CStrategy base class received the Volume() method returning the total volume of open virtual positions, since the developed CVolumeReceiver recipient class needed data on open virtual volumes of each strategy. However, by doing so, we have cut off the ability to open virtual positions for several symbols within one instance of a trading strategy. In this case, the total volume loses its meaning. This solution is suitable for testing single-symbol strategies, but nothing more.
- We used the array for storing the pointers to EA strategies in the CReceiver class, so that the recipient can use them to find out the open virtual volume of the strategies. This led to duplication of the code that fills the strategy arrays in the EA and the receiver.
- Each strategy opens positions on a single symbol: when adding a recipient to the array of strategies, the strategy is asked for its symbol, and it is added to the array of used symbols. We used this feature in the CVolumeReceiver class. The receiver then works only with the symbols added to its symbol array. We have already mentioned the limitation generated as a result of this behavior.
- Let's clean up the CStrategy and CAdvisor base classes as much as possible. Let's write the custom derived CVirtualStrategy and CVirtualAdvisor classes to create the development branch of EAs using virtual trading. They will now be our parent classes for specific strategies and EAs.
- It is time to expand the class of virtual positions. Each virtual position should receive a pointer to a recipient object, which will be responsible for bringing the virtual trading volume to the market, and a trading strategy object, which will make decisions about opening/closing a virtual position. This will allow for notification of interested entities about the operations of opening/closing virtual positions.
- Let's move the storage of all virtual positions into one array, instead of distributing them across several arrays belonging to strategy instances. Each strategy instance will request several elements from this array for its operation. The owner of the common array will be the recipient of trading volumes.
- There will be only one recipient in one EA. Therefore, we will implement it as Singleton. Its only instance will be available in all necessary places. We will formalize this implementation as the CVirtualReceiver derived class.
- We will add the array of new entities - symbol recipients (CVirtualSymbolReceiver class) - into the recipient. Each symbol receiver will only work with the virtual positions of its symbol, which will be automatically attached to the symbol receiver when opened and unattached when closed.
Cleaning up base classes
Let's leave only the essentials in the CStrategy and CAdvisor base classes. In case of CStartegy, leave only the method for handling the OnTick event and get the following concise code:
//+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy { public: virtual void Tick() = 0; // Handle OnTick events };
Everything else will be located in the class descendants.
In the CAdvisor base class, include a small file Macros.mqh, which contains several useful macros for performing operations with regular arrays:
- APPEND(A, V) — add V element to the end of the A array;
- FIND(A, V, I) — write the A array element, equal to A, to I variable. If the element is not found, then -1 is stored in the I variable;
- ADD(A, V) — add V element to the end of the A array if such an element is not already in the array;
- FOREACH(A, D) — loop through the A array indices (the index will be in the i local variable) performing D actions in the body;
- REMOVE_AT(A, I) — remove an element from the A array at an I index position, shifting subsequent elements and reducing the array size;
- REMOVE(A, V) — remove an element equal to V from the A array
// Useful macros for array operations #ifndef __MACROS_INCLUDE__ #define APPEND(A, V) A[ArrayResize(A, ArraySize(A) + 1) - 1] = V; #define FIND(A, V, I) { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } } #define ADD(A, V) { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } } #define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} } #define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);} #define REMOVE(A, V) { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) } #define __MACROS_INCLUDE__ #endif //+------------------------------------------------------------------+
These macros will be used in other files, as this allows us to make the code more compact but readable and avoid calling additional functions.
We will remove all places, where the recipient was encountered, from the CAdvisor class, as well as leave only the call of the corresponding strategy handlers in the OnTick event handling method. We will receive the following code:
#include "Macros.mqh" #include "Strategy.mqh" //+------------------------------------------------------------------+ //| EA base class | //+------------------------------------------------------------------+ class CAdvisor { protected: CStrategy *m_strategies[]; // Array of trading strategies public: ~CAdvisor(); // Destructor virtual void Tick(); // OnTick event handler virtual void Add(CStrategy *strategy); // Method for adding a strategy }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CAdvisor::~CAdvisor() { // Delete all strategy objects FOREACH(m_strategies, delete m_strategies[i]); } //+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CAdvisor::Tick(void) { // Call OnTick handling for all strategies FOREACH(m_strategies, m_strategies[i].Tick()); } //+------------------------------------------------------------------+ //| Strategy adding method | //+------------------------------------------------------------------+ void CAdvisor::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); // Add the strategy to the end of the array } //+------------------------------------------------------------------+
These classes will remain in the Strategy.mqh and Advisor.mqh files in the current folder.
Now let's move the necessary code to the derived strategy and EA classes, which should work with virtual positions.
Create the CVirtualStrategy class inherited from CStrategy. Let's add the following fields and methods to it:
- array of virtual positions (orders);
- total number of open positions and orders;
- method for counting open virtual positions and orders;
- methods for handling opening/closing a virtual position (order).
#include "Strategy.mqh" #include "VirtualOrder.mqh" //+------------------------------------------------------------------+ //| Class of a trading strategy with virtual positions | //+------------------------------------------------------------------+ class CVirtualStrategy : public CStrategy { protected: CVirtualOrder *m_orders[]; // Array of virtual positions (orders) int m_ordersTotal; // Total number of open positions and orders virtual void CountOrders(); // Calculate the number of open positions and orders public: virtual void OnOpen(); // Event handler for opening a virtual position (order) virtual void OnClose(); // Event handler for closing a virtual position (order) }; //+------------------------------------------------------------------+ //| Counting open virtual positions and orders | //+------------------------------------------------------------------+ void CVirtualStrategy::CountOrders() { m_ordersTotal = 0; FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; }) } //+------------------------------------------------------------------+ //| Event handler for opening a virtual position (order) | //+------------------------------------------------------------------+ void CVirtualStrategy::OnOpen() { CountOrders(); } //+------------------------------------------------------------------+ //| Event handler for closing a virtual position (order) | //+------------------------------------------------------------------+ void CVirtualStrategy::OnClose() { CountOrders(); }
Save this code in the VirtualStrategy.mqh file of the current folder.
Since we removed working with the recipient from the CAdvisor base class, then it should be transferred to our new CVirtualAdvisor child class. In this class, we will add the m_receiver field for storing the pointer to the object of the recipient of trading volumes.
In the constructor, the field will be initialized with the pointer to the only possible recipient object, which will be created exactly when the CVirtualReceiver::Instance() static method is called. The destructor will make sure the object is properly deleted.
We will also add new actions in the OnTick event handler. Before launching the handlers for this event in the strategies, we will first launch the handler for this event in the recipient. After the event is handled by the strategies, we will launch the recipient's method that adjusts the open volumes. If the recipient is now the owner of all virtual positions, then they themselves are able to determine the presence of changes. Therefore, there is no implementation of tracking changes in the trading strategy class, so we are removing it not only from the base strategy class, but completely.
#include "Advisor.mqh" #include "VirtualReceiver.mqh" //+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: CVirtualReceiver *m_receiver; // Receiver object that brings positions to the market public: CVirtualAdvisor(ulong p_magic = 1); // Constructor ~CVirtualAdvisor(); // Destructor virtual void Tick() override; // OnTick event handler }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) : // Initialize the receiver with a static receiver m_receiver(CVirtualReceiver::Instance(p_magic)) {}; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { delete m_receiver; // Remove the recipient } //+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Adjusting market volumes m_receiver.Correct(); } //+------------------------------------------------------------------+
Save this code in the VirtualAdvisor.mqh file of the current folder.
Expanding the class of virtual positions
The virtual position class receives the pointer to the m_receiver and m_strategy objects. The values for these fields will have to be passed through the constructor parameters, so we will make changes to it as well. I also added a couple of getters for the private properties of the virtual position: Id() and Symbol(). Let's show the added code in the class description:
//+------------------------------------------------------------------+ //| Class of virtual orders and positions | //+------------------------------------------------------------------+ class CVirtualOrder { private: //--- Static fields... //--- Related recipient objects and strategies CVirtualReceiver *m_receiver; CVirtualStrategy *m_strategy; //--- Order (position) properties ... //--- Closed order (position) properties ... //--- Private methods public: CVirtualOrder( CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy ); // Constructor //--- Methods for checking the position (order) status ... //--- Methods for receiving position (order) properties ... ulong Id() { // ID return m_id; } string Symbol() { // Symbol return m_symbol; } //--- Methods for handling positions (orders) ... };
In the constructor implementation, we simply added two lines to the initialization list to set the values of new fields from the constructor parameters:
CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) : // Initialization list m_id(++s_count), // New ID = object counter + 1 m_receiver(p_receiver), m_strategy(p_strategy), ..., m_point(0) { }
Notification of the recipient and strategy should only occur when a virtual position is opened or closed. This only happens in the Open() and Close() methods, so let's add a little code to them:
//+------------------------------------------------------------------+ //| Open a virtual position | //+------------------------------------------------------------------+ bool CVirtualOrder::Open(...) { // If the position is already open, then do nothing ... if(s_symbolInfo.Name(symbol)) { // Select the desired symbol // Update information about current prices ... // Initialize position properties ... // Depending on the direction, set the opening price, as well as the SL and TP levels ... // Notify the recipient and the strategy that the position (order) is open m_receiver.OnOpen(GetPointer(this)); m_strategy.OnOpen(); ... return true; } return false; } //+------------------------------------------------------------------+ //| Close a position | //+------------------------------------------------------------------+ void CVirtualOrder::Close() { if(IsOpen()) { // If the position is open ... // Define the closure reason to be displayed in the log ... // Save the close price depending on the type ... // Notify the recipient and the strategy that the position (order) is open m_receiver.OnClose(GetPointer(this)); m_strategy.OnClose(); } }
Pass the pointer to the current virtual position object to the OnOpen() and OnClose() recipient handlers. There has not yet been a need for this in strategy handlers, so they are implemented without a parameter.
This code remains in the current folder in the file with the same name — VirtualOrder.mqh.
Implementing a new recipient
Let's start implementing the CVirtualReceiver receiver class to ensure the uniqueness of an instance of a given class. To do this, we will use a standard design pattern called Singleton. We will need to:
- make the class constructor non-public;
- add a static class field that stores the pointer to the class object;
- add a static method that creates, if absent, one instance of this class or returns an already existing one.
//+------------------------------------------------------------------+ //| Class for converting open volumes to market positions (receiver) | //+------------------------------------------------------------------+ class CVirtualReceiver : public CReceiver { protected: // Static pointer to a single class instance static CVirtualReceiver *s_instance; ... CVirtualReceiver(ulong p_magic = 0); // Private constructor public: //--- Static methods static CVirtualReceiver *Instance(ulong p_magic = 0); // Singleton - creating and getting a single instance ... }; // Initializing a static pointer to a single class instance CVirtualReceiver *CVirtualReceiver::s_instance = NULL; //+------------------------------------------------------------------+ //| Singleton - creating and getting a single instance | //+------------------------------------------------------------------+ CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) { if(!s_instance) { s_instance = new CVirtualReceiver(p_magic); } return s_instance; }
Next, add the m_orders array for storing all virtual positions to the class. Each strategy instance will request a certain number of virtual positions from the recipient. To do this, add the Get() static method, which will create the required number of virtual position objects, adding pointers to them to the recipient array and the strategy virtual position array:
class CVirtualReceiver : public CReceiver { protected: ... CVirtualOrder *m_orders[]; // Array of virtual positions ... public: //--- Static methods ... static void Get(CVirtualStrategy *strategy, CVirtualOrder *&orders[], int n); // Allocate the necessary amount of virtual positions to the strategy ... }; ... //+------------------------------------------------------------------+ //| Allocate the necessary amount of virtual positions to strategy | //+------------------------------------------------------------------+ static void CVirtualReceiver::Get(CVirtualStrategy *strategy, // Strategy CVirtualOrder *&orders[], // Array of strategy positions int n // Required number ) { CVirtualReceiver *self = Instance(); // Receiver singleton ArrayResize(orders, n); // Expand the array of virtual positions FOREACH(orders, orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects APPEND(self.m_orders, orders[i])) // Register the created virtual position ... }
Now it is time to add the array for pointers to symbol recipient objects (the CVirtualSymbolReceiver class) into the class. This class has not yet been created, but we already understand what it should do - directly open and close market positions in accordance with virtual volumes for a single symbol. Therefore, we can say that the number of symbol recipient objects will be equal to the number of different symbols used in the EA. We will make its class a descendant of CReceiver, so it will have the Correct() method, which does the main useful work. We will also add the necessary auxiliary methods.
Let's leave this for later and now get back to the CVirtualReceiver class and add the virtual override of the Correct() method to it.
class CVirtualReceiver : public CReceiver { protected: ... CVirtualSymbolReceiver *m_symbolReceivers[]; // Array of recipients for individual symbols public: ... //--- Public methods virtual bool Correct() override; // Adjustment of open volumes };
The implementation of the Correct() method is now quite simple, since we transfer the main work to a lower level of the hierarchy. For now, it is sufficient for us to simply loop through all the symbol recipients and call their Correct() method.
To reduce the number of unnecessary calls, we will add a preliminary check that trading is now generally allowed by adding the IsTradeAllowed() method, which answers the question. We will also add the m_isChanged class field, which should serve as a flag of changes in open virtual positions. We will also check it before calling for an adjustment.
class CVirtualReceiver : public CReceiver { ... bool m_isChanged; // Are there any changes in open positions? ... bool IsTradeAllowed(); // Is trading available? public: ... virtual bool Correct() override; // Adjustment of open volumes }; //+------------------------------------------------------------------+ //| Adjust open volumes | //+------------------------------------------------------------------+ bool CVirtualReceiver::Correct() { bool res = true; if(m_isChanged && IsTradeAllowed()) { // If there are changes, then we call the adjustment of the recipients of individual symbols FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct()); m_isChanged = !res; } return res; }
In the IsTradeAllowed() method, check the status of the terminal and trading account to determine whether it is possible to conduct real trading:
//+------------------------------------------------------------------+ //| Is trading available? | //+------------------------------------------------------------------+ bool CVirtualReceiver::IsTradeAllowed() { return (true && MQLInfoInteger(MQL_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && AccountInfoInteger(ACCOUNT_TRADE_EXPERT) && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_CONNECTED) ); }
We applied the change flag in the Correct() method. The flag was reset if the volume adjustment was successful. But where should this flag be set? Obviously, this should happen if any virtual position is opened or closed. In the CVirtualOrder class, we specifically added the calls for OnOpen() and OnClose() methods, not yet present in the CVirtualReceiver class, to the open/close methods. We will set the changes flag in them.
In addition, we should notify the desired symbol recipient about the changes in these handlers. When opening the very first virtual position for a certain symbol, the corresponding symbol recipient does not yet exist, so we need to create and notify it. During subsequent operations of opening/closing virtual positions for a given symbol, there is already a corresponding symbol recipient, so you just need to notify it.
class CVirtualReceiver : public CReceiver { ... public: ... //--- Public methods void OnOpen(CVirtualOrder *p_order); // Handle virtual position opening void OnClose(CVirtualOrder *p_order); // Handle virtual position closing ... }; //+------------------------------------------------------------------+ //| Handle opening a virtual position | //+------------------------------------------------------------------+ void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) { string symbol = p_order.Symbol(); // Define position symbol CVirtualSymbolReceiver *symbolReceiver; int i; FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient if(i == -1) { // If not found, then create a new recipient for the symbol symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol); // and add it to the array of symbol recipients APPEND(m_symbolReceivers, symbolReceiver); } else { // If found, then take it symbolReceiver = m_symbolReceivers[i]; } symbolReceiver.Open(p_order); // Notify the symbol recipient about the new position m_isChanged = true; // Remember that there are changes } //+------------------------------------------------------------------+ //| Handle closing a virtual position | //+------------------------------------------------------------------+ void CVirtualReceiver::OnClose(CVirtualOrder *p_order) { string symbol = p_order.Symbol(); // Define position symbol int i; FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient if(i != -1) { m_symbolReceivers[i].Close(p_order); // Notify the symbol recipient about closing a position m_isChanged = true; // Remember that there are changes } }
In addition to opening/closing virtual positions based on trading strategy signals, they can be closed when StopLoss or TakeProfit levels are reached. In the CVirtualOrder class, we have the Tick() method specifically for this. It checks the levels and closes the virtual position if necessary. It should be called at every tick and for all virtual positions. This is exactly what the Tick() method in the CVirtualReceiver class will do. Let's add the class:
class CVirtualReceiver : public CReceiver { ... public: ... //--- Public methods void Tick(); // Handle a tick for the array of virtual orders (positions) ... }; //+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ void CVirtualReceiver::Tick() { FOREACH(m_orders, m_orders[i].Tick()); }
Finally, take care of correctly freeing the memory allocated for virtual position objects. Since they are all in the m_orders array, add a destructor, in which we will delete them:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CVirtualReceiver::~CVirtualReceiver() { FOREACH(m_orders, delete m_orders[i]); // Remove virtual positions }
Save the resulting code in the VirtualReceiver.mqh file of the current folder.
Implementing a symbol receiver
It remains to implement the last class CVirtualSymbolReceiver, so that the operation mode takes on a finished form suitable for use. We will take its main content from the CVolumeReceiver class from the previous article, removing places related to determining the symbol of each virtual position and enumerating symbols while performing the adjustment.
Objects of this class will also have their own arrays of pointers to virtual position objects, but here their composition will constantly change. We will require that this array contains only open virtual positions. Then it becomes clear what to do when opening and closing a virtual position: as soon as the virtual position becomes open, we should add it to the array of the corresponding symbol recipient, and as soon as it is closed, remove it from the array.
It will also be convenient for us to have a flag of the presence of changes in the composition of open virtual positions. This will help avoid unnecessary checks on every tick.
Let's add fields for a symbol, an array of positions and a flag of changes, as well as two methods for handling opening/closing, to the class:
class CVirtualSymbolReceiver : public CReceiver { string m_symbol; // Symbol CVirtualOrder *m_orders[]; // Array of open virtual positions bool m_isChanged; // Are there any changes in the composition of virtual positions? ... public: ... void Open(CVirtualOrder *p_order); // Register opening a virtual position void Close(CVirtualOrder *p_order); // Register closing a virtual position ... };
The implementation of these methods itself is trivial: we add/remove the passed virtual position from the array and set the flag for the presence of changes.
//+------------------------------------------------------------------+ //| Register opening a virtual position | //+------------------------------------------------------------------+ void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) { APPEND(m_orders, p_order); // Add a position to the array m_isChanged = true; // Set the changes flag } //+------------------------------------------------------------------+ //| Register closing a virtual position | //+------------------------------------------------------------------+ void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) { REMOVE(m_orders, p_order); // Remove a position from the array m_isChanged = true; // Set the changes flag }
We also need to search for the desired symbol recipient by a symbol name. In order to use the normal linear search algorithm from the FIND(A,V,I) macro, let's add an overloaded operator comparing the symbol recipient to the string that returns 'true' if the instance symbol matches the passed string:
class CVirtualSymbolReceiver : public CReceiver { ... public: ... bool operator==(const string symbol) {// Operator for comparing by a symbol name return m_symbol == symbol; } ... };
Here is a complete description of the CVirtualSymbolReceiver class. Find the specific implementation of all methods in the attached files.
class CVirtualSymbolReceiver : public CReceiver { string m_symbol; // Symbol CVirtualOrder *m_orders[]; // Array of open virtual positions bool m_isChanged; // Are there any changes in the composition of virtual positions? bool m_isNetting; // Is this a netting account? double m_minMargin; // Minimum margin for opening CPositionInfo m_position; // Object for obtaining properties of market positions CSymbolInfo m_symbolInfo; // Object for getting symbol properties CTrade m_trade; // Object for performing trading operations double MarketVolume(); // Volume of open market positions double VirtualVolume(); // Volume of open virtual positions bool IsTradeAllowed(); // Is trading by symbol available? // Required volume difference double DiffVolume(double marketVolume, double virtualVolume); // Volume correction for the required difference bool Correct(double oldVolume, double diffVolume); // Auxiliary opening methods bool ClearOpen(double diffVolume); bool AddBuy(double volume); bool AddSell(double volume); // Auxiliary closing methods bool CloseBuyPartial(double volume); bool CloseSellPartial(double volume); bool CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type); bool CloseFull(); // Check margin requirements bool FreeMarginCheck(double volume, ENUM_ORDER_TYPE type); public: CVirtualSymbolReceiver(ulong p_magic, string p_symbol); // Constructor bool operator==(const string symbol) {// Operator for comparing by a symbol name return m_symbol == symbol; } void Open(CVirtualOrder *p_order); // Register opening a virtual position void Close(CVirtualOrder *p_order); // Register closing a virtual position virtual bool Correct() override; // Adjustment of open volumes };
Save this code in the VirtualSymbolReceiver.mqh file of the current folder.
Comparing results
The resulting operation mode can be represented as follows:
Fig. 3. Operation mode from the current article
Now comes the most interesting part. Let's compile the EA that uses nine instances of strategies with the same parameters as in the previous article. Let's perform test runs of a similar EA from the previous article and the one we have just compiled:
Fig. 4. Results of the EA from the previous article
Fig. 5. Results of the EA from the current article
In general, the results are almost the same. The balance graphs are generally indistinguishable. Small differences visible in the reports may be due to various reasons and will be analyzed further.
Assessing further potential
In the discussion of the previous article, a logical question was asked: what are the most attractive trading results that can be obtained using the approach in question? So far, the graphs have shown a return of 20% over 5 years, which does not look particularly attractive.
For now, the answer to this question can be as follows. First, it is necessary to clearly separate the results due to the selected simple strategies and the results due to the implementation of their joint work.
The results of the first category will change when changing a simple strategy to another. It is clear that the better the results shown by individual instances of simple strategies, the better their overall result will be. The results presented here were obtained on one trading idea, and were initially determined precisely by its quality and suitability. We evaluate these results simply by the profit/drawdown ratio for the test interval.
The results of the second category are the comparative results of joint and solo work. Here the assessment is made based on other parameters: improving the linearity of the graph of the equity growth curve, reducing drawdown, and others. It is these results that seem more important, since there is hope to bring the not particularly outstanding results of the first category to an acceptable level with their help.
But for all results, it is advisable for us to first implement variable lot trading. Without this, it is more difficult to even estimate the profitability/drawdown ratio based on test results, although it is still possible.
Let's try to take a small initial deposit and select a new optimal value for the size of open positions for the maximum allowed drawdown of 50% for a period of 5 years (2018.01.01 — 2023.01.01). Below are the results of EA runs from this article with a different position size multiplier, but constant for all five years with an initial deposit of USD 1000. In the previous article, position sizes were calibrated to the deposit size of USD 10,000, so the initial depoPart_ value was reduced by about 10 times.
Fig. 6. Test results with different position sizes
We see that with minimal depoPart_= 0.04, the EA did not open real positions, since their volume when recalculated in proportion to the balance is less than 0.01. But starting from the next multiplier value depoPart_= 0.06, market positions were opened.
At the maximum depoPart_ = 0.4, we get the profit of approximately USD 22,800. However, the drawdown shown here is the relative drawdown encountered over the entire run. But 10% of 23,000 and 1000 are very different values. Therefore, we should definitely look at the results of a single run:
Fig. 7. Test results at maximum depoPart_ = 0.4
As you can see, the drawdown of USD 1167 was actually reached, which at the time of achievement was only 9.99% of the current balance, but if the beginning of the test period was located immediately before this unpleasant moment, then we would have lost the entire deposit. Therefore, we cannot use this position size.
Let's look at the results when depoPart_ = 0.2
Fig. 8. Test results at depoPart_ = 0.2
Here the maximum drawdown did not exceed USD 494, that is, about 50% of the initial deposit of USD 1000. Therefore, with such a position size, even if the beginning of the period during the five years under consideration is chosen as poorly as possible, there will be no loss of the entire deposit.
With this position size, the test results for 1 year (2022) will be as follows:
Fig. 9. Test results for 2022 at depoPart_ = 0.2
So, with the maximum expected drawdown of approximately 50%, we see the profit of about 150% per year.
These results look encouraging, but there is a fly in the ointment. The results for 2023, which was not included in the parameter optimization, are noticeably worse:
Fig. 10. Test results for 2023 at depoPart_ = 0.2
Of course, we received the 40% profit in the test results at the end of the year, but there was no sustainable growth 8 out of 12 months. This problem seems to be the main one, and this series of articles will be devoted to considering different approaches to solving it.
Conclusion
In this article, we have prepared for further development of the code by simplifying and optimizing the code from the previous part. We have addressed some previously identified deficiencies that could limit our ability to utilize various trading strategies. The test results showed that the new implementation works no worse than the previous one. The speed of the implementation remained unchanged, but it is possible that the growth will only appear with a multiple increase in the number of strategy instances.
To do this, we need to finally figure out how we will store the input parameters of strategies, how we will combine them into parameter libraries, and how we will select the best combinations from those that will be obtained as a result of optimizing single strategy instances.
We will continue working in the chosen direction in the next article.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/14148
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
It would be a good idea to add an on/off strategy mask to account for the volumetrician (recipient of virtual volumes).
For example, you need to switch off some TS from the portfolio for a while: it continues to trade virtually, but does not affect the real environment. Similarly with its reverse switching on.
It is not difficult to realise such a thing, but without using it, you will need clear on/off criteria for each strategy. This is a more complex task, I haven't approached it yet; probably I won't.
And there must be a competent incorporation of something like this somewhere.
CAdvisor *m_advisors[]; // Массив виртуальных портфелей
with its own mages.
There are no plans for that. Merging into portfolios will happen at an intermediate level between CAdvisor and CStrategy. There is a draft solution, but it is likely to change a lot during the ongoing refactoring.
The flag that volume synchronisation was successful can save you.
It seems to be already there:
It is reset only when the required real volume is successfully opened for each symbol.
Putting a completely different entity into a virtual order is a questionable solution.
I really wanted to avoid this. I searched in every possible way how to leave CVirtualOrder unrelated to these entities. I liked what I got even less. That's why this is how it is for now.