MQL's OOP notes: Converting MetaTrader 4 indicators to MetaTrader 5

28 September 2016, 23:26
Stanislav Korotky
12
4 401
It has been a long time since MetaTrader 5 was released, but MQL products for MetaTrader 4 do still prevail on mql5.com site (both in the codebase, and in the market), and in the Internet in general. This is why it's often a problem that an interesting indicator or expert adviser exists for MetaTrader 4 but is missing for MetaTrader 5. Today we'll consider how OOP can help in converting MetaTrader 4's indicators to MetaTrader 5's in a snap.

The process implies that there should be a header file (.mqh) with all required stuff, which could be sufficient to include into a mq4-file, and after changing its extension to .mq5 compilation should produce a ready-made indicator for MetaTrader 5.

Of course, the mqh-file should contain some non-OOP defines and functions. For example, here is a part of them:

#define StrToDouble StringToDouble
#define DoubleToStr DoubleToString
#define TimeToStr TimeToString
#define Ask SymbolInfoDouble(_Symbol, SYMBOL_ASK)
#define Bid SymbolInfoDouble(_Symbol, SYMBOL_BID)

#define Digits _Digits
#define Point _Point

#define extern input

...

int ObjectsTotal()
{
  return ObjectsTotal(0);
}

bool ObjectCreate(const string name, const ENUM_OBJECT type, const int subwindow, const datetime time1, const double price1)
{
  return ObjectCreate(0, name, type, subwindow, time1, price1);
};

...

bool ObjectSetText(const string name, const string text, const int fontsize = 0)
{
  bool b = ObjectSetString(0, name, OBJPROP_TEXT, text);
  if(fontsize != 0) ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontsize);
  return b;
}

...

int iBarShift(string symbol, ENUM_TIMEFRAMES timeframe, datetime time, bool Exact = true)
{
  datetime lastBar;
  SeriesInfoInteger(symbol, timeframe, SERIES_LASTBAR_DATE, lastBar);
  return(Bars(symbol, timeframe, time, lastBar) - 1);
}

...

long iVolume(string symbol, ENUM_TIMEFRAMES tf, int b)
{
  long result[1];
  return CopyTickVolume(symbol, tf, b, 1, result) > 0 ? result[0] : 0;
}

...

bool _SetIndexBuffer(const int index, double &buffer[], const ENUM_INDEXBUFFER_TYPE type = INDICATOR_DATA)
{
  bool b = ::SetIndexBuffer(index, buffer, type);
  ArraySetAsSeries(buffer, true);
  ArrayInitialize(buffer, EMPTY_VALUE); // default filling
  return b;
}

#define SetIndexBuffer _SetIndexBuffer

void SetIndexStyle(const int index, const int type, const int style = EMPTY, const int width = EMPTY, const color clr = clrNONE)
{
  PlotIndexSetInteger(index, PLOT_DRAW_TYPE, type);
  if(style != EMPTY) PlotIndexSetInteger(index, PLOT_LINE_STYLE, style);
  if(width != EMPTY) PlotIndexSetInteger(index, PLOT_LINE_WIDTH, width);
  if(clr != clrNONE) PlotIndexSetInteger(index, PLOT_LINE_COLOR, clr);
}

All of these is obvious. You'll find complete code in the attachment.

Most interesting parts deal with some features which require OOP for indicator transformation.

First, remember MT4 accessors for timeseries - Open[], High[], Low[], Close[], etc. We can mimic this by objects with overloaded operator[]. For example, for volume data we can use the class:

class VolumeBroker
{
  public:
    long operator[](int b)
    {
      return iVolume(_Symbol, _Period, b);
    }
};

VolumeBroker Volume;

Actually, as all these classes are very similar to each other, one can generate them by preprocessor using one define:

#define DefineBroker(NAME,TYPE) \
class NAME##Broker \
{ \
  public: \
    TYPE operator[](int b) \
    { \
      return i##NAME(_Symbol, _Period, b); \
    } \
}; \
NAME##Broker NAME;

DefineBroker(Time, datetime);
DefineBroker(Open, double);
DefineBroker(High, double);
DefineBroker(Low, double);
DefineBroker(Close, double);
DefineBroker(Volume, long);

Second, remember that object property setter function ObjectSet has changed in MT5 to a set of functions ObjectSetInteger, ObjectSetDouble, etc, accepting different number of parameters. For example, mql4-code:

ObjectSet(name, OBJPROP_TIME1, Time[0]);
ObjectSet(name, OBJPROP_PRICE1, price);

should be somehow translated into mql5:

ObjectSetInteger(0, name, OBJPROP_TIME, 0, Time[0]);
ObjectSetDouble(0, name, OBJPROP_PRICE, 0, price);

It's important that there are no constants such as OBJPROP_TIME1 and OBJPROP_PRICE1 in mql5, and instead of OBJPROP_TIME1, OBJPROP_TIME2, OBJPROP_TIME3 one need to use the single constant OBJPROP_TIME and additional index. Price-related constants changed in the same way. How to emulate this? Well, by objects. Let's define 2 classes for properties of integer and double types (other types you can add yourself).

class OBJPROP_INTEGER_BROKER
{
  public:
    ENUM_OBJECT_PROPERTY_INTEGER p;
    int i;
    
    OBJPROP_INTEGER_BROKER(const ENUM_OBJECT_PROPERTY_INTEGER property, const int modifier)
    {
      p = property;
      i = modifier;
    }
};

class OBJPROP_DOUBLE_BROKER
{
  public:
    ENUM_OBJECT_PROPERTY_DOUBLE p;
    int i;
    
    OBJPROP_DOUBLE_BROKER(const ENUM_OBJECT_PROPERTY_DOUBLE property, const int modifier)
    {
      p = property;
      i = modifier;
    }
};

OBJPROP_INTEGER_BROKER OBJPROP_TIME1(OBJPROP_TIME, 0);
OBJPROP_DOUBLE_BROKER OBJPROP_PRICE1(OBJPROP_PRICE, 0);
OBJPROP_INTEGER_BROKER OBJPROP_TIME2(OBJPROP_TIME, 1);
OBJPROP_DOUBLE_BROKER OBJPROP_PRICE2(OBJPROP_PRICE, 1);
OBJPROP_INTEGER_BROKER OBJPROP_TIME3(OBJPROP_TIME, 2);
OBJPROP_DOUBLE_BROKER OBJPROP_PRICE3(OBJPROP_PRICE, 2);

Look how constants OBJPROP_TIMEn/OBJPROP_PRICEn "became" the objects storing corresponding index inside. Now we can implement new ObjectSet functions, and pass broker objects as parameters:

bool ObjectSet(const string name, const OBJPROP_INTEGER_BROKER &property, const long value)
{
  return ObjectSetInteger(0, name, property.p, property.i, value);
}

bool ObjectSet(const string name, const OBJPROP_DOUBLE_BROKER &property, const double value)
{
  return ObjectSetDouble(0, name, property.p, property.i, value);
}

Voila!

ObjectSet(name, OBJPROP_TIME1, Time[0]);
ObjectSet(name, OBJPROP_PRICE1, price);

works again in MetaTrader 5. 

Resulting mqh-file is attached below. To convert an indicator, just include the file into mq4-file and rename it to .mq5.

Please note that a couple of lines of code should be added manually anyway, because some completely new language structures have been introduced in MetaTrader 5, so there is no chance to generate them based on mql4 source code.

Let us consider a process of convertion of several indicators.

Let's take standard ZigZag.mq4 from MetaQuotes (specifically, dated 2014). You should add indicator_plots directive and change indicator_buffers number:

#property indicator_plots 1   // added
#property indicator_buffers 3 // was 1

because this is how MetaTrader 5 adds auxiliary buffers, and IndicatorBuffers is not supported anymore (it should be deleted in the source code). Also lines:

SetIndexBuffer(1, ExtHighBuffer);
SetIndexBuffer(2, ExtLowBuffer);

should be edited as follows:

SetIndexBuffer(1, ExtHighBuffer, INDICATOR_CALCULATIONS);
SetIndexBuffer(2, ExtLowBuffer, INDICATOR_CALCULATIONS);

As the indicator uses new style of event handlers (OnInit, OnCalculate) add MT4_NEW_EVENT_HANDLERS define before include

#define MT4_NEW_EVENT_HANDLERS
#include <ind4to5.mqh>

That's it - the indicator is ready to compile (3 lines added, 1 removed, 2 changed).

MT4_NEW_EVENT_HANDLERS define does basically invoke ArraySetAsSeries(,true) for all input arrays of OnCalculate. Not all indicators require this, so this define is optional (use as appropriate).

For example standard Parabolic.mq4 from MetaQuotes (dated 2014 as well) uses ArraySetAsSeries explicitly (on its own), so its conversion is even simplier: just add 2 lines:

#property indicator_plots 1

and

#include <ind4to5.mqh> 

Probably not all portable features specific for MetaTrader 4 are covered in the attached mqh-file and not all possible replacements are provided for MetaTrader 5. Feel free to extend and alter the file as you need.

Please note that MetaTrader 5 is more strict and sensitive for errors in mql code. If an indicator contains a bug, it can still run without a visible issue on MetaTrader 4, but will fail on MetaTrader 5 after convertion due to the bug. Always use #property strict in your codes and fix bugs.

Finally let's take a custom indicator from codebase. It happened to be MaBased-ZigZag.mq4 (as one of most popular). Here we need to change a bit more - partly because it contains bugs obscured by forgiving MetaTrader 4 (and they should be fixed), and partly because... because it invokes other indicator, specifically iMA. Indicators are created and used in MetaTrader 5 in a new way, which we have not yet addressed in this blog post. But there is already a lot of gimmicks for one time, so let us take a break and leave this question for the next publication.   

Files:
ind4to5.mqh  12 kb