Русский 中文 Español Deutsch 日本語 Português
Universal Expert Advisor: CUnIndicator and Use of Pending Orders (Part 9)

Universal Expert Advisor: CUnIndicator and Use of Pending Orders (Part 9)

MetaTrader 5Examples | 20 October 2017, 10:06
12 205 7
Vasiliy Sokolov
Vasiliy Sokolov

Table of Contents

Introduction

The development of the Universal Expert Advisor's base of source code continues. Most of the approaches integrated into the CStrategy trade engine have proven their efficiency, convenience and simplicity in practice. However, over the course of actual usage, certain aspects in the expert operation had to be reviewed.

One of such aspects is working with indicators. The third article of this series proposed a classic object-oriented approach to working with indicators. The idea behind it was treating each indicator as an object-oriented class, with their own methods for setting and getting certain properties. In practice, however, implementing an individual wrapper class for each indicator is difficult to accomplish. This article considers a new way of working with indicators in OOP style, with no implementation of separate module classes required.

The second change considered in the article is associated with introduction of a procedure for full-scale management of pending orders. If the pending orders had to managed directly in the code of the resulting strategy before, a part of such functions is now delegated to the CStrategy trade engine. The final strategy class is can now override the SupportBuyPending and SupportSellPending methods and start managing pending orders, similar to managing active positions.

Accessing indicators in the earlier CStrategy versions

To understand the task at hand, let us refer to the solution for working with indicators from the aforementioned third article of the series. It proposed working with any indicator through a wrapper class. For instance, the provided example used the CIndMovingAverage wrapper class for working with the iMA indicator. The CIndMovingAverage class itself consisted of methods that set or return a particular property of the indicator, as well as the Init method which called the iMA system function. Even though the indicator class had a simple structure, its code was still quite lengthy, which users were required to implement themselves. See the source code of this class to estimate the amount of work assigned to it:

//+------------------------------------------------------------------+
//|                                                MovingAverage.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Message.mqh>
#include <Strategy\Logs.mqh>
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
class CIndMovingAverage
  {
private:
   int               m_ma_handle;         // Indicator handle
   ENUM_TIMEFRAMES   m_timeframe;         // Timeframe
   int               m_ma_period;         // Period
   int               m_ma_shift;          // Shift
   string            m_symbol;            // Symbol
   ENUM_MA_METHOD    m_ma_method;         // Moving Average method
   uint              m_applied_price;     // The handle of the indicator, on which you want to calculate the Moving Average value,
                                          // or one of price values of ENUM_APPLIED_PRICE
   CLog*             m_log;               // Logging
   void              Init(void);
public:
                     CIndMovingAverage(void);

/*Params*/
   void              Timeframe(ENUM_TIMEFRAMES timeframe);
   void              MaPeriod(int ma_period);
   void              MaShift(int ma_shift);
   void              MaMethod(ENUM_MA_METHOD method);
   void              AppliedPrice(int source);
   void              Symbol(string symbol);

   ENUM_TIMEFRAMES   Timeframe(void);
   int               MaPeriod(void);
   int               MaShift(void);
   ENUM_MA_METHOD    MaMethod(void);
   uint              AppliedPrice(void);
   string            Symbol(void);

/*Out values*/
   double            OutValue(int index);
  };
//+------------------------------------------------------------------+
//| Default constructor                                              |
//+------------------------------------------------------------------+
CIndMovingAverage::CIndMovingAverage(void) : m_ma_handle(INVALID_HANDLE),
                                             m_timeframe(PERIOD_CURRENT),
                                             m_ma_period(12),
                                             m_ma_shift(0),
                                             m_ma_method(MODE_SMA),
                                             m_applied_price(PRICE_CLOSE)
  {
   m_log=CLog::GetLog();
  }
//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
CIndMovingAverage::Init(void)
  {
   if(m_ma_handle!=INVALID_HANDLE)
     {
      bool res=IndicatorRelease(m_ma_handle);
      if(!res)
        {
         string text="Realise iMA indicator failed. Error ID: "+(string)GetLastError();
         CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
         m_log.AddMessage(msg);
        }
     }
   m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price);
   if(m_ma_handle==INVALID_HANDLE)
     {
      string params="(Period:"+(string)m_ma_period+", Shift: "+(string)m_ma_shift+
                    ", MA Method:"+EnumToString(m_ma_method)+")";
      string text="Create iMA indicator failed"+params+". Error ID: "+(string)GetLastError();
      CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text);
      m_log.AddMessage(msg);
     }
  }
//+------------------------------------------------------------------+
//| Setting the timeframe                                            |
//+------------------------------------------------------------------+
void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Returns the current timeframe                                    |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(void)
  {
   return m_timeframe;
  }
//+------------------------------------------------------------------+
//| Sets the Moving Average averaging period                         |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaPeriod(int ma_period)
  {
   m_ma_period=ma_period;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Returns the current averaging period of Moving Average           |
//+------------------------------------------------------------------+
int CIndMovingAverage::MaPeriod(void)
  {
   return m_ma_period;
  }
//+------------------------------------------------------------------+
//| Sets the Moving Average type                                     |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method)
  {
   m_ma_method=method;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Returns the Moving Average type                                  |
//+------------------------------------------------------------------+
ENUM_MA_METHOD CIndMovingAverage::MaMethod(void)
  {
   return m_ma_method;
  }
//+------------------------------------------------------------------+
//| Returns the Moving Average shift                                 |
//+------------------------------------------------------------------+
int CIndMovingAverage::MaShift(void)
  {
   return m_ma_shift;
  }
//+------------------------------------------------------------------+
//| Sets the Moving Average shift                                    |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaShift(int ma_shift)
  {
   m_ma_shift=ma_shift;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Sets the type of the price used for MA calculation               |
//+------------------------------------------------------------------+
void CIndMovingAverage::AppliedPrice(int price)
  {
   m_applied_price = price;
   if(m_ma_handle != INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Returns the type of the price used for MA calculation            |
//+------------------------------------------------------------------+
uint CIndMovingAverage::AppliedPrice(void)
  {
   return m_applied_price;
  }
//+------------------------------------------------------------------+
//| Sets the symbol to calculate the indicator for                   |
//+------------------------------------------------------------------+
void CIndMovingAverage::Symbol(string symbol)
  {
   m_symbol=symbol;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Returns the symbol the indicator is calculated for               |
//+------------------------------------------------------------------+
string CIndMovingAverage::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| Returns the value of the MA with index 'index'                   |
//+------------------------------------------------------------------+
double CIndMovingAverage::OutValue(int index)
  {
   if(m_ma_handle==INVALID_HANDLE)
      Init();
   double values[];
   if(CopyBuffer(m_ma_handle,0,index,1,values))
      return values[0];
   return EMPTY_VALUE;
  }

Obviously, the amount of code is impressive. And this is merely an example of one of the simplest indicators. This approach is complicated by the fact that hundreds of various indicators have been created for MetaTrader, both standard and custom ones. Each of them has its own, relatively unique set of properties and parameters. According to the proposed approach, a separate wrapper needs to be created for each such indicator. 

CStrategy allows addressing the indicators directly, with no other classes involved. Therefore, in practice I often called a specific system function in the code of the trading strategy. After all, it is much easier to call an indicator using standard methods rather than spending time on writing the corresponding class.

IndicatorCreate function — the basis of the universal interface

As it often happens, the solution was found after some practical use of CStrategy. It became obvious that the indicator accessing mechanism should have the following properties:

  • Versatility. Access to any indicator must be based on a generalized access procedure, rather than a multitude of wrapper classes.
  • Convenience and simplicity. Access to the indicator values should be convenient and simple. It must not depend on the indicator type.

The practical application has shown that the simplest, and more importantly, universal access to an indicator usually occurs though the iCustom and IndicatorCreate functions.

Both functions allow creating standard and custom indicators. The iCustom function requires the actual parameters of the indicator to be specified at the program compilation time. The IndicatorCreate function is arranged differently. It takes the MqlParams array of structures. It is the format of the second function that makes it possible to create a generalized procedure of access to an arbitrary indicator. In this case, the indicator parameters do not need to be known beforehand, which makes the access procedure really universal.

Let us consider a concrete example of working with IndicatorCreate. We will use it to create a handle of the Moving Average indicator. It will be the same indicator as returned by the iMa indicator:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Get the indicator handle using iMA
   int h_ima = iMA(Symbol(), Period(), 13, 0, MODE_SMA, PRICE_CLOSE); 
   // Get the same indicator handle using IndicatorCreate
   MqlParam ma_params_1[4];
   ma_params_1[0].type = TYPE_INT;
   ma_params_1[0].integer_value = 13;
   ma_params_1[1].type = TYPE_INT;
   ma_params_1[1].integer_value = 0;
   ma_params_1[2].type = TYPE_INT;
   ma_params_1[2].integer_value = MODE_SMA;
   ma_params_1[3].type = TYPE_INT;
   ma_params_1[3].integer_value = PRICE_CLOSE;
   int h_ma_c = IndicatorCreate(Symbol(), Period(), IND_MA, 4, ma_params_1);
   if(h_ima == h_ma_c)
      printf("Indicator handles are equal");
   else
      printf("Indicator handles are not equal");
}

The presented script accessed the same indicator through two different interfaces: functions iMA and IndicatorCreate. Both functions return the same handle, which is easy to verify: once launched, the script outputs "Indicator handles are equal". However, indicator access through IndicatorCreate involves tedious configuration of the MqlParams array. Two properties must be set for each element of the MqlParam array: type of the variable and value of this variable. The IndicatorCreate function is seldom used mainly due to this cumbersomeness. However, it is this call interface that allows accessing absolutely any MQL indicator. Therefore, we will use it.

CUnIndicator — the universal indicator CStrategy

Object-oriented programming helps in hiding most of the configuration of the MqlParams array elements from the user, providing a convenient interface for setting an arbitrary parameter. Let us create CUnIndicator — a wrapper class for the IndicatorCreate function. It can be used to sequentially set an arbitrary number of parameters for an indicator. It will not be necessary to specify the type of a particular parameter. Thanks to templates, the passed type will be detected automatically. This class will also have convenient indexers in the form of square brackets '[]'. It will be possible to set both the indicator value index, and the time for which the value must be obtained.

The work with CUnIndicator will be reduced to the following steps.

  • Setting the required parameters using the SetParameter method
  • Directly creating the indicator using the Create method
  • Setting the required buffer through SetBuffer (optional)
  • Accessing the indicator value at arbitrary index i using the [] indexer
  • Removing the indicator using the IndicatorRelease method (optional).

Here is a simple script that creates the Moving Average indicator using CUnIndicator:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
CUnIndicator UnMA;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Get the indicator handle using iMA
   int h_ima = iMA(Symbol(), Period(), 13, 0, MODE_SMA, PRICE_CLOSE); 
   // Get the indicator handle using CUnIndicator
   UnMA.SetParameter(13);
   UnMA.SetParameter(0);
   UnMA.SetParameter(MODE_SMA);
   int handle = UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");
}

The 'handle' variable now contains the handle of the created indicator, and the UnMA object allows working with the indicator values. For example, to get the indicator value on the previous bar, the following line is sufficient:

double value = UnMA[1];

Let us consider a more complex example. Create an indicator that contains several buffers — the standard MACD, for instance. Each line has a detailed comment:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
input int FastEMA = 12;
input int SlowEMA = 26;
input int SignalSMA = 9;

CUnIndicator UnMACD;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   UnMACD.SetParameter(FastEMA);                            // Set the value of the fast moving average
   UnMACD.SetParameter(SlowEMA);                            // Set the value of the slow moving average
   UnMACD.SetParameter(SignalSMA);                          // Set the period of the signal line
   int handle = UnMACD.Create(Symbol(), Period(),           // Create the indicator, specifying the symbol and the timeframe
                "Examples\\MACD", PRICE_CLOSE);
   UnMACD.SetBuffer(MAIN_LINE);                             // Set the default buffer - MACD values
   double macd = UnMACD[1];                                 // Get the value of MACD on the previous bar
   UnMACD.SetBuffer(SIGNAL_LINE);                           // Set the signal line as the default buffer
   double signal = UnMACD[1];                               // Get the value of the signal line on the previous bar
   datetime time_span = TimeCurrent() - PeriodSeconds();    // Calculate the opening time of the previous bar
   double signal_by_time = UnMACD[time_span];               // Get the value of the signal line for the time
   printf("MACD: " + DoubleToString(macd, Digits()) +       // Output the values of MACD and the signal line on the previous bar
         "; Signal: " + DoubleToString(signal, Digits()));
   if(signal == signal_by_time)                             // Access by time and index will match
      printf("the values taken by the index and time are equal ");
}
//+------------------------------------------------------------------+

The most interesting part in this example is switching internal indicator buffers using the SetBuffer method. Thus, the value returned by UnMACD[1] will differ depending on which buffer is currently set. The first time, UnMACD[1] returns the value of MACD on the previous bar. However, if SIGNAL_LINE is set as the default buffer, UnMACD[1] returns the value of the signal line.

The indicator values can be accessed both by index and by time. In the example, the 'time_span' is calculated, which is equal to the previous bar open time. If this time is specified in UnMACD instead of the index, the returned value will be the same as for UnMACD[1].

Internal structure of CUnIndicator

It is time to analyze the internal structure of CUnIndicator. Its full source code is as follows:

//+------------------------------------------------------------------+
//|                                                   Indicators.mqh |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include "Message.mqh"
#include "Logs.mqh"
//+------------------------------------------------------------------+
//| Indicator base class                                             |
//+------------------------------------------------------------------+
class CUnIndicator
{
private:
   MqlParam m_params[];
   int      m_params_count;
   int      m_current_buffer;
   int      m_handle;
   static   CLog*    Log;
   bool     m_invalid_handle;
   void     PushName(string name);
public:
            CUnIndicator(void);
   void     SetBuffer(int index);
   template <typename T>
   bool     SetParameter(T value);
   int      Create(string symbol, ENUM_TIMEFRAMES period, string name);
   int      Create(string symbol, ENUM_TIMEFRAMES period, string name, int app_price);
   int      Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type);
   int      Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type, int app_price);
   void     InitByHandle(int handle);
   void     IndicatorRelease(void);
   double   operator[](int index);
   double   operator[](datetime time);
   int      GetHandle(void);
};
CLog        *CUnIndicator::Log;
//+------------------------------------------------------------------+
//| Initialization without specifying the name                       |
//+------------------------------------------------------------------+
CUnIndicator::CUnIndicator(void) : m_params_count(0),
                                   m_handle(INVALID_HANDLE),
                                   m_current_buffer(0),
                                   m_invalid_handle(false)
{
   Log = CLog::GetLog(); 
}

//+------------------------------------------------------------------+
//| Indicator deinitialization                                       |
//+------------------------------------------------------------------+
CUnIndicator::IndicatorRelease(void)
{
   if(m_handle != INVALID_HANDLE)
      IndicatorRelease(m_handle);
   ArrayResize(m_params, 1);
   m_params_count = 1;
   m_current_buffer = 0;
   m_handle = INVALID_HANDLE;
}

template <typename T>
bool CUnIndicator::SetParameter(T value)
{
   
   string type = typename(value);
   MqlParam param;
   if(type == "string")
   {
      param.type = TYPE_STRING;
      param.string_value = (string)value;
   }
   else if(type == "int")
   {
      param.type = TYPE_INT;
      param.integer_value = (long)value;
   }
   else if(type == "double")
   {
      param.type = TYPE_DOUBLE;
      param.double_value = (double)value;
   }
   else if(type == "bool")
   {
      param.type = TYPE_BOOL;
      param.integer_value = (int)value;
   }
   else if(type == "datetime")
   {
      param.type = TYPE_DATETIME;
      param.integer_value = (datetime)value;
   }
   else if(type == "color")
   {
      param.type = TYPE_COLOR;
      param.integer_value = (color)value;
   }
   else if(type == "ulong")
   {
      param.type = TYPE_ULONG;
      param.integer_value = (long)value;
   }
   else if(type == "uint")
   {
      param.type = TYPE_UINT;
      param.integer_value = (uint)value;
   }
   else
   {
      param.type = TYPE_INT;
      param.integer_value = (int)value;
   }
   m_params_count++;
   if(ArraySize(m_params) < m_params_count)
      ArrayResize(m_params, m_params_count);
   m_params[m_params_count-1].double_value = param.double_value;
   m_params[m_params_count-1].integer_value = param.integer_value;
   m_params[m_params_count-1].string_value = param.string_value;
   m_params[m_params_count-1].type = param.type;
   return true;
}
//+------------------------------------------------------------------+
//| Returns the indicator handle                                     |
//+------------------------------------------------------------------+
int CUnIndicator::GetHandle(void)
{
   return m_handle;
}
//+------------------------------------------------------------------+
//| Sets the current indicator buffer                                |
//+------------------------------------------------------------------+
void CUnIndicator::SetBuffer(int index)
{
   m_current_buffer = index;
}
//+------------------------------------------------------------------+
//| Initialize the indicator (create its handle). Returns the handle |
//| of indicator if successful, otherwise INVALID_HANDLE if the      |
//| indicator creation failed                                        |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, string name)
{
   PushName(name);
   m_handle = IndicatorCreate(symbol, period, IND_CUSTOM, m_params_count, m_params);
   if(m_handle == INVALID_HANDLE && m_invalid_handle == false)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Check its params. Last error:" + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
   }
   return m_handle;
}
//+------------------------------------------------------------------+
//| Initialize the indicator (create its handle). Returns the handle |
//| of indicator if successful, otherwise INVALID_HANDLE if the      |
//| indicator creation failed                                        |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type)
{
   if(ind_type == IND_CUSTOM)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Indicator type can not be IND_CUSTOM";
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
      return INVALID_HANDLE;
   }
   m_handle = IndicatorCreate(symbol, period, ind_type, m_params_count, m_params);
   if(m_handle == INVALID_HANDLE && m_invalid_handle == false)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Check its params. Last error:" + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
   }
   return m_handle;
}
//+------------------------------------------------------------------+
//| Initialize the indicator (create its handle). Returns the handle |
//| of indicator if successful, otherwise INVALID_HANDLE if the      |
//| indicator creation failed                                        |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol,ENUM_TIMEFRAMES period,ENUM_INDICATOR ind_type,int app_price)
{
   SetParameter(app_price);
   return Create(symbol, period, ind_type);
}
//+------------------------------------------------------------------+
//| Places the indicator name at the zero index of m_params[] array  |
//+------------------------------------------------------------------+
void CUnIndicator::PushName(string name)
{
   int old_size = ArraySize(m_params);
   int size = ArrayResize(m_params, ArraySize(m_params) + 1);
   for(int i = 0; i < old_size; i++)
      m_params[i+1] = m_params[i];
   m_params[0].type = TYPE_STRING;
   m_params[0].string_value = name;
}
//+------------------------------------------------------------------+
//| Initialize the indicator (create its handle). Returns the handle |
//| of indicator if successful, otherwise INVALID_HANDLE if the      |
//| indicator creation failed                                        |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, string name, int app_price)
{
   SetParameter(app_price);
   return Create(symbol, period, name);
}
//+------------------------------------------------------------------+
//| Initializes an indicator class based on an existing              |
//| indicator handle                                                 |
//+------------------------------------------------------------------+
void CUnIndicator::InitByHandle(int handle)
{
   this.IndicatorRelease();
   m_handle = handle;
}
//+------------------------------------------------------------------+
//| Returns the indicator value based on 'index'                     |
//+------------------------------------------------------------------+
double CUnIndicator::operator[](int index)
{
   double values[];
   if(m_handle == INVALID_HANDLE)
      return EMPTY_VALUE;
   if(CopyBuffer(m_handle, m_current_buffer, index, 1, values) == 0)
   {
      string text = "Failed copy buffer of indicator. Last error: " + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      return EMPTY_VALUE;
   }
   return values[0];
}
//+------------------------------------------------------------------+
//| Returns the indicator value based on 'time'                      |
//+------------------------------------------------------------------+
double CUnIndicator::operator[](datetime time)
{
   double values[];
   if(m_handle == INVALID_HANDLE)
      return EMPTY_VALUE;
   
   if(CopyBuffer(m_handle, m_current_buffer, time, 1, values) == 0)
   {
      string text = "Failed copy buffer of indicator. Last error: " + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      return EMPTY_VALUE;
   }
   return values[0];
}

From the code listing above, it can be seen than SetParameter is a template method. It takes the universal T argument as input, depending on which the appropriate type of the ENUM_DATATYPE parameter is selected. This parameter is set for the MqlParam structure. Multiple string checks are not optimal in the context of speed. This, however, does not affect the performance, as the SetParameter function is to be called only once — during the initialization of the Expert Advisor.

The class is supplied with numerous variants of the Create method. This provides the ability to create both custom indicators (specifying the string name of the indicator) and standard indicators, the type of which can be set via INDICATOR_TYPE. For example, this is how the moving average can be created as a custom indicator:

UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");

Here, UnMA is an instance of CUnIndicator. Creation of the same indicator as a standard one happens slightly differently:

UnMA.Create(Symbol(), Period(), IND_MA);

The CUnIndicator class also contains the InitByHandle method. Let us dwell on it in more detail. As it is known, many indicators can be calculated not only based on the symbol prices, but also based on the data of another indicator. Thanks to this, it is possible to create a chain of indicators that calculate their values based on the output values of the previous one. Suppose that we need to calculate the values of Stochastic based on the moving average. To do this, it is necessary to initialize two indicators: one for calculating the moving average, the other one is for Stochastic. This can be done the following way:

//+------------------------------------------------------------------+
//|                                                        Test3.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
input int MaPeriod = 12;
input int StochK = 3;
input int StochD = 12;
input int StochSmoth = 3;

CUnIndicator UnSMA;
CUnIndicator UnStoch;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Set the parameters and create the SMA indicator
   UnSMA.SetParameter(12);
   UnSMA.SetParameter(0);
   UnSMA.SetParameter(MODE_SMA);
   UnSMA.SetParameter(PRICE_CLOSE);
   int sma_h = UnSMA.Create(Symbol(), Period(),
      "\\Examples\Custom Moving Average");
   // Set the parameters and create the Stoch indicator,
   // based on the data from SMA
   UnStoch.SetParameter(StochK);
   UnStoch.SetParameter(StochD);
   UnStoch.SetParameter(StochSmoth);
   UnStoch.InitByHandle(sma_h);
   double stoch_on_sma = UnStoch[1];
}
//+------------------------------------------------------------------+

The code above shows that the handle of the indicator created by the Create method is stored and used for creating the Stochastic indicator. In case a custom indicator is used, the data source does not have to be specified. However, such source must be specified for the system indicators. This can be done in two ways: via the SetParameter method:

UnMA.SetParameter(PRICE_CLOSE);

As well as using the overloaded version of the Create method:

UnMA.Create(Symbol(), Period(), IND_MA, PRICE_CLOSE);

We will later create a demo trading system that accesses the indicator values via the CUnIndicator class.

Improved work with pending orders

One of the previous articles devoted to CStrategy introduced the CPendingOrders object-oriented class, representing a pending order in CStrategy. CPendingOrders is an interface class. It contains no internal members, except the field for storing the order ticket. All its methods get the corresponding properties by calling the three main system functions — OrderGetInterer, OrderGetDouble and OrderGetString. Such organization ensures the integrity of the data representation. An instance of CPendingOrders corresponds to each pending order is MetaTrader 5, with the ticket equal to that real order. If the pending order is canceled for some reason (by an expert or by the user), then the CStrategy trade engine deletes the corresponding CPendingOrders instance from the list of pending orders. The CPendingOrders list is stored as a special COrdersEnvironment class. Each strategy has its own instance of COrdersEnvironment, called PendingOrders. The strategy could directly access this object and select the required order from the object by index.

If the strategy needed to open a pending order instead of a market order, it simply sent a corresponding trade request via the CTrade module. In this regard, placement of a pending order had no difference from placement of a market order. But then the differences started, which were not taken into account in CStrategy. CStrategy is arranged in such a way that each market position is passed to the handler code in turn. Thus, positions of type POSITION_TYPE_BUY are handled by the SupportBuy method, and positions of type POSITION_TYPE_SELL have the SupportSell method. With pending orders, everything was different. Each such order was placed in the PendingOrders collection accessible by the expert, but such orders had no event handler of their own. It was assumed that the pending orders would be processed somewhere else, for example, in OnEvent, BuySupport/SellSupport or even in BuyInit/SellInit. In this case, however, if there were no open positions, no calls to BuySupport/SellSupport would follow; and, consequently, a reliable handling of pending orders would only be possible in OnEvent. But handling in this method violated the general sequence of actions. It turned out that a part of positions was sequentially processed in a queue arranged by CStrategy, while another part of positions (pending orders) was processed in the old manner, in a single OnEvent block.

Due to this, the new version of CStrategy introduces two additional methods, SupportPendingBuy and SupportPendingSell:

class CStrategy
{
protected:
   ...   
   virtual void      SupportPendingBuy(const MarketEvent &event, CPendingOrder* order);
   virtual void      SupportPendingSell(const MarketEvent &event, CPendingOrder* order);
   ...
};

Their signature is similar to SupportBuy and SupportSell methods: the MarketEvent event is passed as the first parameter, the second one is a pointer to the current order selected by CStrategy. The order itself is selected by the CStrategy engine, using the exhaustive search method. The search is performed in the CallSupport method, from the end of the orders list to its beginning:

//+------------------------------------------------------------------+
//| Calls the position management logic, provided that the trading   |
//| state isn't equal to TRADE_WAIT.                                 |
//+------------------------------------------------------------------+
void CStrategy::CallSupport(const MarketEvent &event)
  {
   ...
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* order = PendingOrders.GetOrder(i);
      if(order.ExpertMagic()!=m_expert_magic)continue;
      if(order.Direction() == POSITION_TYPE_BUY)
         SupportPendingBuy(event, order);
      else
         SupportPendingSell(event, order);
   }
}

Thus, the handlers for pending orders are called the same way as for positions in the market: the SupportPendingBuy method is called for each pending buy order, and the SupportPendingSell method is called for each pending sell order.

The full cycle of the strategy, which works with pending orders, turns out to be longer than a full cycle of the strategy based only on market orders. In the second case, a sequence of two handlers is used for each direction:

  1. opening a long position in InitBuy / opening a short position in InitSell;
  2. managing the long position in SupportBuy / managing the short position in SupportSell.

When using the strategy for pending orders, it is necessary to employ three handlers per direction:

  1. placing a pending long position in InitBuy / placing a pending short position in InitSell;
  2. managing the pending long position in SupportPendingBuy until it is triggered or canceled / managing the pending short position in SupportPendingSell until it is triggered or canceled;
  3. managing the long position in SupportBuy / managing the short position in SupportSell.

In fact, management of pending orders is always an independent part of the logic of the trading strategy. Therefore, a separated management of pending orders and market positions made it possible to reduce the complexity of developing such strategies.

The CImpulse 2.0 strategy 

The best way to deal with the new changes is to rewrite the familiar example of the CImpulse trading strategy, which was presented in the fifth part of the article. Its essence lies in placing a pending Stop order on each bar, at a certain distance from the moving average. The distance is expressed as a percentage. For a buy deal, a BuyStop order is placed, with the opening level above the moving average by N percent. For a sell deal, it is the opposite: a SellStop order is placed, with the opening level below the moving average by N percent. The position will be closed once the Close price becomes lower (for buy deals) or higher (for sell deals) than the moving average.

In general, this is the same strategy as the one presented earlier, but in a new revised edition. It can be used as an example for evaluating the changes made in CStrategy and to see how the new features can be used in practice. But first, we need to turn to the code of the EA from the previous version. It will be provided here fully, so that both syntax versions could be compared later:

//+------------------------------------------------------------------+
//|                                                      Impulse.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>

input double StopPercent = 0.20;
//+------------------------------------------------------------------+
//| Defines the actions that need to be performed with a pending     |
//| order.                                                           |
//+------------------------------------------------------------------+
enum ENUM_ORDER_TASK
{
   ORDER_TASK_DELETE,   // Delete a pending order
   ORDER_TASK_MODIFY    // Modify a pending order
};
//+------------------------------------------------------------------+
//| The CImpulse strategy                                            |
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // Percent value for the level of a pending order
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
public:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CIndMovingAverage Moving;  // The work with the moving average is carried out through a class specifically written for it
};
//+------------------------------------------------------------------+
//| Working with the pending BuyStop orders for opening a long       |
//| position                                                         |
//+------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_buy > 0) return;                    
   int buy_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Ask() + Ask()*(m_percent/100.0);
   if(target < Moving.OutValue(0))                    // The order trigger price must be above the Moving Average
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--) // Iterating over pending orders in the InitBuy section, which is not very good
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_BUY_STOP)
      {
         if(task == ORDER_TASK_MODIFY) // The work with pending orders is carried out through a system of states
         {
            buy_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(buy_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| Working with the pending SellStop orders for opening a short     |
//| position                                                         |
//+------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_sell > 0) return;                    
   int sell_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Bid() - Bid()*(m_percent/100.0);
   if(target > Moving.OutValue(0))                    // The order trigger price must be below the Moving Average
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_SELL_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            sell_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(sell_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| Managing a long position in accordance with the Moving Average   |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Bid() < Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Managing a short position in accordance with the Moving Average  |
//+------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Ask() > Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Filters incoming events. If the passed event is not              |
//| processed by the strategy, returns false; if it is processed     |
//| returns true.                                                    |
//+------------------------------------------------------------------+
bool CImpulse::IsTrackEvents(const MarketEvent &event)
  {
//We handle only opening of a new bar on the working symbol and timeframe
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Respond to the symbol change                                     |
//+------------------------------------------------------------------+
void CImpulse::OnSymbolChanged(string new_symbol)
  {
   Moving.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| Respond to the timeframe change                                  |
//+------------------------------------------------------------------+
void CImpulse::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   Moving.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| Returns the percent of the breakthrough level                    |
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| Sets percent of the breakthrough level                           |
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

The most problematic parts in implementation of this strategy are highlighted in yellow.

Firstly, the work with the indicator is carried out through the CIndMovingAverage class created previously. This approach has already been deemed unreasonable. There are far too many indicators to write a class for each of them.

Secondly, the work with pending orders is carried out through an exhaustive search of pending orders in the BuyInit/SellInit blocks. This causes no complications in a simple strategy, such as CImpulse. But in the case of a more sophisticated order management, difficulties may arise. It is better to separate the placement of pending orders and their management process into standalone methods, as done in the new version of CStrategy.

A closer look at the code of CImpulse reveals that the part of functionality, which is to be provided by CStrategy, is undertaken by CImpulse. CStrategy is supposed to define a system of states for managing pending orders, but it does not: the system is implemented by CImpulse itself.

Let us rewrite the code with new features of CStrategy in mind:

//+------------------------------------------------------------------+
//|                                                  Impulse 2.0.mqh |
//|           Copyright 2017, Vasiliy Sokolov, St-Petersburg, Russia |
//|                                https://www.mql5.com/en/users/c-4 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com/en/users/c-4"
#include "Strategy\Strategy.mqh"
#include "Strategy\Indicators.mqh"

input int PeriodMA = 12;
input double StopPercent = 0.05;

//+------------------------------------------------------------------+
//| The CImpulse strategy                                            |
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // Percent value for the level of a pending order
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      SupportPendingBuy(const MarketEvent &event,CPendingOrder *order);
   virtual void      SupportPendingSell(const MarketEvent &event,CPendingOrder* order);
   virtual bool      OnInit(void);
public:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CUnIndicator      UnMA;
};
//+------------------------------------------------------------------+
//| Initialize the moving average                                    |
//+------------------------------------------------------------------+
bool CImpulse::OnInit(void)
{
   UnMA.SetParameter(12);
   UnMA.SetParameter(0);
   UnMA.SetParameter(MODE_SMA);
   UnMA.SetParameter(PRICE_CLOSE);
   m_percent = StopPercent;
   if(UnMA.Create(Symbol(), Period(), IND_MA) != INVALID_HANDLE)
      return true;
   return false;
}
//+------------------------------------------------------------------+
//| Placing pending BuyStop orders                                   |
//+------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                                           // Create pending only at the opening of a new bar
   if(PositionsTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > 0)   // There must be no open long positions present
      return;
   if(OrdersTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > 0)      // There must be no pending buy order present
      return;
   double target = WS.Ask() + WS.Ask()*(m_percent/100.0);                     // Calculate the level of the new pending order
   if(target < UnMA[0])                                                       // The order trigger price must be above the Moving Average
      return;
   Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);       // Place the new BuyStop order
}
//+------------------------------------------------------------------+
//| Working with the pending BuyStop orders for opening a long       |
//| position                                                         |
//+------------------------------------------------------------------+
void CImpulse::SupportPendingBuy(const MarketEvent &event,CPendingOrder *order)
{
   if(!IsTrackEvents(event))return;
   double target = WS.Ask() + WS.Ask()*(m_percent/100.0);                     // Calculate the level of the new pending order
   if(UnMA[0] > target)                                                       // If the new level is lower than the current Moving Average
      order.Delete();                                                         // - delete it
   else                                                                       // Otherwise, modify it with the new price
      order.Modify(target);
}
//+------------------------------------------------------------------+
//| Working with the pending SellStop orders for opening a short     |
//| position                                                         |
//+------------------------------------------------------------------+
void CImpulse::SupportPendingSell(const MarketEvent &event,CPendingOrder* order)
{
   if(!IsTrackEvents(event))return;
   double target = WS.Ask() - WS.Ask()*(m_percent/100.0);                     // Calculate the level of the new pending order
   if(UnMA[0] < target)                                                       // If the new level is higher than the current Moving Average
      order.Delete();                                                         // - delete it
   else                                                                       // Otherwise, modify it with the new price
      order.Modify(target);
}
//+------------------------------------------------------------------+
//| Placing pending SellStop orders                                  |
//+------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                                           // Create pending only at the opening of a new bar
   if(PositionsTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > 0)  // There must be no open short positions present
      return;
   if(OrdersTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > 0)     // There must be no pending sell order present
      return;
   double target = WS.Bid() - WS.Bid()*(m_percent/100.0);                     // Calculate the level of the new pending order
   if(target > UnMA[0])                                                       // The order trigger price must be below the Moving Average
      return;  
   Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);      // Place the new BuyStop order
}
//+------------------------------------------------------------------+
//| Managing a long position in accordance with the Moving Average   |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   int bar_open = WS.IndexByTime(pos.TimeOpen());
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = WS.Bid() - WS.Bid()*(m_percent/100.0);
      if(target < UnMA[0])
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(WS.Bid() < UnMA[0])
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Managing a short position in accordance with the Moving Average  |
//+------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = WS.Ask() + WS.Ask()*(m_percent/100.0);
      if(target > UnMA[0])
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(WS.Ask() > UnMA[0])
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Returns the percent of the breakthrough level                    |
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| Sets percent of the breakthrough level                           |
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   CImpulse* impulse = new CImpulse();
   impulse.ExpertMagic(140578);
   impulse.ExpertName("Impulse 2.0");
   impulse.Timeframe(Period());
   impulse.ExpertSymbol(Symbol());
   Manager.AddStrategy(impulse);
   return(INIT_SUCCEEDED);
   
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   Manager.OnTick();
}
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
{
   Manager.OnChartEvent(id, lparam, dparam, sparam);
   ChartRedraw(0);
}

The screenshot below shows a test fragment of CIpmulse 2.0 in the strategy tester. It shows the placed pending orders and the work with them:


Fig. 1. Working with pending orders during the test of the Impulse 2.0 strategy

Despite the logic remaining the same, the old and the new code turned out different. Here are the differences between the new and the old version:

  • The current instrument is accesses via an instance of CSymbol WS (work symbol). All properties of the symbol are now delegated to this class.
  • The Expert Advisor data are initialized in the OnInit method. At the time of writing the fifth part of the article, it was not available yet.
  • Instead of the CIndMovingAverage indicator, the universal CUnIndicator indicator is now used.
  • Placement of pending orders is carried out in InitBuy/InitSell, and their modification and deletion are in SupportPendingBuy/SupportPendingSell.
  • Iteration over pending orders is no longer used. This function is delegated to CStrategy.
  • Instead of the 'position' structure, the PositionsTotal and OrdersTotal methods are now used for calculating the number of currently open positions and pending orders.

The provided code listing contains the strategy code, as well as the basic functions of the expert. In other words, this example is presented as a single .mq5 file. This is due to the fact that the structure of the project has been substantially reorganized. We will dwell on this below.

The new structure of the CStrategy project

In previous versions, the CStrategy trade engine was located in several MQL5 subfolders. For example, the engine itself and its auxiliary files were located in MQL5\Include\Strategy. The source codes responsible for the implementation of the engine's panel were in MQL5\Include\Panel. The expert code could be located in MQL5\Include\Strategy\Samples, and the mq5 file for launching the EA — in MQL5\Experts\Article08. And all this in addition to the fact that various auxiliary components, such as Dictionary.mqh or XmlBase.mqh, were also scattered across many catalogs of the Include folder.

Obviously, the formed interconnections in the project had become very complicated, and the location of files and directories is often duplicated. This makes it difficult to understand the CStrategy trade engine. A user, especially a beginner, can easily get confused and become unable to understand where everything comes from and how the compilation process goes. Therefore, starting with the current version of the trade engine, its files are placed differently.

The whole project is now located in the MQL5\Experts\UnExpert directory. It contains the Strategy folder and strategy files with the .mq5 extension. The final trading strategy is now a single mq5 file, which contains both standard functions for handling events (such as OnInit and OnTick), and the strategy class itself, based on CStrategy.

All auxiliary files have also been placed to MQL5\Experts\UnExpert\Strategy. This also applies to files for working with XML, and infrastructure files (such as Dictionary.mqh). To compile the example, simply open the file (for instance, "MQL5\Experts\UnExpert\Impulse 2.0.mqh") and press the "Compile" button.

In the build provided in this part of the article as an example, only two strategies are used — Impulse 1.0 and Impulse 2.0. It is the same strategy, but written in the old and new style of CStrategy. This was done on purpose, so that you can compare both approaches in action and see the differences described in this article. Other sample strategies that were included in the previous versions of CStrategy are not available in this build. This is because they rely on the old syntax, and therefore cannot be presented as an example. Perhaps they will appear in the future versions, but with a revised syntax.

Conclusion

The new components of CStrategy have been discussed. These include the CUnIndicator class, which implements a universal OOP style interface for working with any system or custom MQL5 indicator, as well as a system for managing pending orders based on the SupportPendingBuy and SupportPendingSell methods. All these elements combined provide a powerful synergetic effect when writing an Expert Advisor. The user does not need to think about low-level operations. Almost all the trading environment is accessible through intuitive and concise classes, and the trading logic can be defined by simply overriding the appropriate preset methods.

The project is now located in one place, and its connections are limited to the MQL5\Experts\UnExpert catalog. Now there is no need to place files in different folders across the MQL5 directory. This innovation should also encourage users to move on to CStrategy, or at least to study its features with more interest.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/2653

Attached files |
UnExpert.zip (680.21 KB)
Last comments | Go to discussion (7)
Alessandro
Alessandro | 12 Mar 2021 at 12:01
Hello everyone, I have recently registered in this comunity, I am trying to learn this language better and better, and here, I often read the comments and I have found a world of people trying to help each other, and this it's very beautiful. About this series, I must say that it is very very interesting and programmed at a high level. However, I would like to have a clarification. In the Expert published in this last article (9), I noticed that "Manager.OnTick ()" is executed in the OnTick function in the main file (Impulse 2.0.mq5). In this method you can check new ticks and new candles also of many other currency pairs (talking about Forex). But the Manager.OnTick() method is only executed when a Tick occurs in the currency pair where the EA is executed, so if here, for example no ticks occur for a minute, how can you check the ticks in another currency pair?
Thanks .. I hope you can clarify this doubt.
Hello everybody.
Alexander
[Deleted] | 20 May 2021 at 07:20

Excellent piece of code and sample.

But useless for newbies as most of the examples will not work without compilation error and you need to have same level of knowledge as the writer of article to debug them. 

FLB
FLB | 5 Oct 2021 at 22:19

Seems that some methods are defined like constructors, therefore the compilation errors.

You have to add void in front of the wrong code.

Example:

Message.mqh could not be compiled because of the following error:

'Init' - unexpected token, probably type is missing?    Message.mqh     80      11
'Init' - function already defined and has different type        Message.mqh     80      11

Code looks like this:

CMessage::Init(ENUM_MESSAGE_TYPE type,string source,string text)

All you have to do is to add a void in front of the line:

void CMessage::Init(ENUM_MESSAGE_TYPE type,string source,string text)

There are several files affected (e.g. Dictionary.mhq as shown in first post) but after correcting them, the code will compile without errors.

28846173
28846173 | 26 Oct 2022 at 20:44

KINDLY FIX YOUR CODE TO BE COMPATIBLE WITH CURRENT BUILD


Scott Allen
Scott Allen | 15 Jun 2023 at 22:15
FLB #:

You have to add void in front of the wrong code.

[...]

There are several files affected (e.g. Dictionary.mhq as shown in first post) but after correcting them, the code will compile without errors.

Yes, this got it to compile without errors, and the EA will run in the Strategy Tester, but it makes no trades. Tried on multiple symbols and timeframes, and on default values on all symbols in market watch.

Don't even know where to begin debugging it.  Anyone else gotten this to work?

Cross-Platform Expert Advisor: The CExpertAdvisor and CExpertAdvisors Classes Cross-Platform Expert Advisor: The CExpertAdvisor and CExpertAdvisors Classes
This article deals primarily with the classes CExpertAdvisor and CExpertAdvisors, which serve as the container for all the other components described in this article-series regarding cross-platform expert advisors.
Graphical Interfaces XI: Integrating the Standard Graphics Library (build 16) Graphical Interfaces XI: Integrating the Standard Graphics Library (build 16)
A new version of the graphics library for creating scientific charts (the CGraphic class) has been presented recently. This update of the developed library for creating graphical interfaces will introduce a version with a new control for creating charts. Now it is even easier to visualize data of different types.
Auto search for divergences and convergences Auto search for divergences and convergences
The article considers all kinds of divergence: simple, hidden, extended, triple, quadruple, convergence, as well as divergences of A, B and C classes. A universal indicator for their search and display on the chart is developed.
Risk Evaluation in the Sequence of Deals with One Asset Risk Evaluation in the Sequence of Deals with One Asset
This article describes the use of methods of the theory of probability and mathematical statistics in the analysis of trading systems.