Русский 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Object-Oriented Approach to Building Multi-Timeframe and Multi-Currency Panels

Object-Oriented Approach to Building Multi-Timeframe and Multi-Currency Panels

MetaTrader 5Examples | 20 December 2011, 13:15
11 327 3
Marcin Konieczny
Marcin Konieczny

Introduction

This article describes how object-oriented programming can be used for creating multi-timeframe and multi-currency panels for MetaTrader 5. The main goal is to build a universal panel, which can be used for displaying many different kinds of data, such as prices, price changes, indicator values or custom buy/sell conditions without the need to modify the code of the panel itself. In this way very little coding will be necessary to customize the panel in any way we need.

The solution I will describe works in two modes:

  1. Multi-timeframe mode - allows to see the table contents calculated on current symbol, but on different timeframes;
  2. Multi-currency mode - allows to see the table contents calculated on current timeframe, but on different symbols.

The following pictures show the panel in these two modes.

First one works in multi-timeframe mode and displays the following data:

  1. Current price;
  2. Current bar's price change;
  3. Current bar's price change as a percent;
  4. Current bar's price change as an arrow (up/down);
  5. RSI(14) indicator value;
  6. RSI(10) indicator value;
  7. Custom condition: SMA(20) > current price.

Figure 1. Mult-timeframe mode

Figure 1. Mult-timeframe mode


Second one works in multi-currency mode and shows:

  1. Current price;
  2. Current bar's price change;
  3. Current bar's price change as a percent;
  4. Current bar's price change as an arrow;
  5. RSI(10) indicator value;
  6. RSI(14) indicator value;
  7. Custom condition: SMA(20) > current price.

Figure 2. Mult-currency mode

Figure 2. Mult-currency mode


1. Implementation

The following class diagram describes the implementation design of the panel.

Figure 3. Class diagram of the panel

Figure 3. Class diagram of the panel


Let me describe the elements of the diagram:

  1. CTable. Core class of the panel. It is responsible for drawing the panel and managing its components.
  2. SpyAgent. It is an indicator responsible for 'spying' on other symbols (instruments). Every agent is created and sent to a different symbol. The agent reacts to OnCalculate event when a new tick arrives on the symbol chart and sends CHARTEVENT_CUSTOM event to inform the CTable object that it has to update. The whole idea behind this approach is based on the article "The Implementation of a Multi-currency Mode in MetaTrader 5". You can find all the technical details there.
  3. CRow. Base class for all indicators and conditions used to create the panel. By extending this class it is possible to create all the necessary components of the panel.
  4. CPriceRow. Simple class extending CRow, which is used to display the current bid price.
  5. CPriceChangeRow. Class extending CRow, used for displaying current bar's price change. It can show price changes, percent changes or arrows.
  6. CRSIRow. Class extending CRow, used for displaying current RSI indicator value.
  7. CPriceMARow. Class extending CRow, showing a custom condition: SMA > current price.

CTable and CRow classes as well as SpyAgent indicator are the core parts of the panel. CPriceRow, CPriceChangeRow, CRSIRow and CPriceMARow are actual contents of the panel. CRow class is designed to be extended by many new classes in order to get the desired result. The four presented derived classes are just simple examples of what can be done and how.


2. SpyAgent

We will begin with the SpyAgent indicator. It is used only in multi-currency mode and is necessary to properly update the panel, when a new tick arrives on other charts. I won't go much into details of this concept. They are described in the article "The Implementation of a Multi-currency Mode in MetaTrader 5".

The SpyAgent indicator runs on the chart of the specified symbol and sends two events: initialization event and new tick event. Both events are of CHARTEVENT_CUSTOM type. In order to handle these events we have to use the OnChartEvent(...) handler (it will be shown later in the article).

Let's have a look at SpyAgent's code:

//+------------------------------------------------------------------+
//|                                                     SpyAgent.mq5 |
//|                                                 Marcin Konieczny |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Marcin Konieczny"
#property indicator_chart_window
#property indicator_plots 0

input long   chart_id=0;        // chart id
input ushort custom_event_id=0; // event id
//+------------------------------------------------------------------+
//| Indicator iteration function                                     |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {

   if(prev_calculated==0)
      EventChartCustom(chart_id,0,0,0.0,_Symbol); // sends initialization event
   else
      EventChartCustom(chart_id,(ushort)(custom_event_id+1),0,0.0,_Symbol); // sends new tick event

   return(rates_total);
  }
It is quite simple. The only thing it does is receiving new ticks and sending CHARTEVENT_CUSTOM events.


3. CTable

CTable is the core class of the panel. It stores information about the panel settings and manages its components. It updates (redraws) the panel, when necessary.

Let's look at the declaration of CTable:

//+------------------------------------------------------------------+
//| CTable class                                                     |
//+------------------------------------------------------------------+
class CTable
  {
private:
   int               xDistance;    // distance from right border of the chart
   int               yDistance;    // distance from top of the chart
   int               cellHeight;   // table cell height
   int               cellWidth;    // table cell width
   string            font;         // font name
   int               fontSize;
   color             fontColor;

   CList            *rowList;      // list of row objects
   bool              tfMode;       // is in multi-timeframe mode?

   ENUM_TIMEFRAMES   timeframes[]; // array of timeframes for multi-timeframe mode
   string            symbols[];    // array of currency pairs for multi-currency mode

   //--- private methods
   //--- sets default parameters of the table
   void              Init();
   //--- draws text label in the specified table cell
   void              DrawLabel(int x,int y,string text,string font,color col);
   //--- returns textual representation of given timeframe
   string            PeriodToString(ENUM_TIMEFRAMES period);

public:
   //--- multi-timeframe mode constructor
                     CTable(ENUM_TIMEFRAMES &tfs[]);
   //--- multi-currency mode constructor
                     CTable(string &symb[]);
   //--- destructor
                    ~CTable();
   //--- redraws table
   void              Update();
   //--- methods for setting table parameters
   void              SetDistance(int xDist,int yDist);
   void              SetCellSize(int cellW,int cellH);
   void              SetFont(string fnt,int size,color clr);
   //--- appends CRow object to the of the table
   void              AddRow(CRow *row);
  };

As you can see, all panel components (rows) are stored as a list of CRow pointers, so every component that we want to add to the panel has to extend CRow class. CRow can be seen as a contract between the panel and its components. CTable does not contain any code for calculation of its cells. It is a responsibility of classes extending CRow. CTable is only a structure for holding CRow components and redrawing them when necessary.

Let's go through the methods of CTable. The class has two constructors. The first is used for multi-timeframe mode and is quite simple. We only have to supply an array of timeframes that we want to display.

//+------------------------------------------------------------------+
//| Multi-timeframe mode constructor                                 |
//+------------------------------------------------------------------+
CTable::CTable(ENUM_TIMEFRAMES &tfs[])
  {
//--- copy all timeframes to own array
   ArrayResize(timeframes,ArraySize(tfs),0);
   ArrayCopy(timeframes,tfs);
   tfMode=true;
   
//--- fill symbols array with current chart symbol
   ArrayResize(symbols,ArraySize(tfs),0);
   for(int i=0; i<ArraySize(tfs); i++)
      symbols[i]=Symbol();

//--- set default parameters
   Init();
  }

The second constructor is used for multi-currency mode and takes an array of symbols (instruments). This one also sends SpyAgents. It attaches them one by one to appropriate charts.

//+------------------------------------------------------------------+
//| Multi-currency mode constructor                                  |
//+------------------------------------------------------------------+
CTable::CTable(string &symb[])
  {
//--- copy all symbols to own array
   ArrayResize(symbols,ArraySize(symb),0);
   ArrayCopy(symbols,symb);
   tfMode=false;
   
//--- fill timeframe array with current timeframe
   ArrayResize(timeframes,ArraySize(symb),0);
   ArrayInitialize(timeframes,Period());

//--- set default parameters
   Init();

//--- send SpyAgents to every requested symbol
   for(int x=0; x<ArraySize(symbols); x++)
      if(symbols[x]!=Symbol()) // don't send SpyAgent to own chart
         if(iCustom(symbols[x],0,"SpyAgent",ChartID(),0)==INVALID_HANDLE)
           {
            Print("Error in setting of SpyAgent on "+symbols[x]);
            return;
           }
  }

The Init method creates the list of rows (as a CList object - CList is a dynamic list of CObject types) and sets the default values for CTable internal variables (font, font size, color, cell dimension and distance from the upper right chart corner).

//+------------------------------------------------------------------+
//| Sets default parameters of the table                             |
//+------------------------------------------------------------------+
CTable::Init()
  {
//--- create list for storing row objects
   rowList=new CList;

//--- set defaults
   xDistance = 10;
   yDistance = 10;
   cellWidth = 60;
   cellHeight= 20;
   font="Arial";
   fontSize=10;
   fontColor=clrWhite;
  }

The destructor is fairly simple. It deletes the list of rows and deletes all chart objects (labels) created by the panel.

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTable::~CTable()
  {
   int total=ObjectsTotal(0);

//--- remove all text labels from the chart (all object names starting with nameBase prefix)
   for(int i=total-1; i>=0; i--)
      if(StringFind(ObjectName(0,i),nameBase)!=-1)
         ObjectDelete(0,ObjectName(0,i));

//--- delete list of rows and free memory
   delete(rowList);
  }

The AddRow method appends the new row to the list of rows. Note that rowList is a CList object, which resizes automatically. This method also calls the Init method for every added CRow object. It is necessary to allow the object to initialize its internal variables properly. For example, it may use the Init call to create indicator or file handles.

//+------------------------------------------------------------------+
//| Appends new row to the end of the table                          |
//+------------------------------------------------------------------+
CTable::AddRow(CRow *row)
  {
   rowList.Add(row);
   row.Init(symbols,timeframes);
  }

The Update method is a bit more complicated. It is used for redrawing the panel.

Basically, it consists of three parts, which are:

  • Drawing the first column (the names of rows)
  • Drawing the first row (the names of timeframes or symbols depending on the selected mode)
  • Drawing the internal cells (the components' values)

It is worth noting that we ask every component to calculate its own value basing on the supplied symbol and timeframe. We also let the component decide, what font and color should be used.

//+------------------------------------------------------------------+
//| Redraws the table                                                |
//+------------------------------------------------------------------+
CTable::Update()
  {
   CRow *row;
   string symbol;
   ENUM_TIMEFRAMES tf;

   int rows=rowList.Total(); // number of rows
   int columns;              // number of columns

   if(tfMode)
      columns=ArraySize(timeframes);
   else
      columns=ArraySize(symbols);

//--- draw first column (names of rows)
   for(int y=0; y<rows; y++)
     {
      row=(CRow*)rowList.GetNodeAtIndex(y);
      //--- note: we ask row object to return its name
      DrawLabel(columns,y+1,row.GetName(),font,fontColor);
     }

//--- draws first row (names of timeframes or currency pairs)
   for(int x=0; x<columns; x++)
     {
      if(tfMode)
         DrawLabel(columns-x-1,0,PeriodToString(timeframes[x]),font,fontColor);
      else
         DrawLabel(columns-x-1,0,symbols[x],font,fontColor);
     }

//--- draws inside table cells
   for(int y=0; y<rows; y++)
      for(int x=0; x<columns; x++)
        {
         row=(CRow*)rowList.GetNodeAtIndex(y);

         if(tfMode)
           {
            //--- in multi-timeframe mode use current symbol and different timeframes
            tf=timeframes[x];
            symbol=_Symbol;
           }
         else
           {
            //--- in multi-currency mode use current timeframe and different symbols
            tf=Period();
            symbol=symbols[x];
           }

         //--- note: we ask row object to return its font, 
         //--- color and current calculated value for given timeframe and symbol
         DrawLabel(columns-x-1,y+1,row.GetValue(symbol,tf),row.GetFont(symbol,tf),row.GetColor(symbol,tf));
        }

//--- forces chart to redraw
   ChartRedraw();
  }

The DrawLabel method is used for drawing text labels in the specified cell of the panel. Firstly, it checks if a label for this cell already exists. If not, it creates a new one.

Then, it sets all necessary label properties and its text.

//+------------------------------------------------------------------+
//| Draws text label in the specified cell of the table              |
//+------------------------------------------------------------------+  
CTable::DrawLabel(int x,int y,string text,string font,color col)
  {
//--- create unique name for this cell
   string name=nameBase+IntegerToString(x)+":"+IntegerToString(y);

//--- create label
   if(ObjectFind(0,name)<0)
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);

//--- set label properties
   ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_RIGHT_UPPER);
   ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
   ObjectSetInteger(0,name,OBJPROP_XDISTANCE,xDistance+x*cellWidth);
   ObjectSetInteger(0,name,OBJPROP_YDISTANCE,yDistance+y*cellHeight);
   ObjectSetString(0,name,OBJPROP_FONT,font);
   ObjectSetInteger(0,name,OBJPROP_COLOR,col);
   ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontSize);

//--- set label text
   ObjectSetString(0,name,OBJPROP_TEXT,text);
  }

Other methods won't be presented here, because they are very simple and less important. The full code can be downloaded at the bottom of the article.


4. Extending CRow

CRow is a base class for all components, which can be used by the panel.

Let's look at the code of the CRow class:

//+------------------------------------------------------------------+
//| CRow class                                                       |
//+------------------------------------------------------------------+
//| Base class for creating custom table rows                        |
//| one or more methods of CRow should be overriden                  |
//| when creating own table rows                                     |
//+------------------------------------------------------------------+
class CRow : public CObject
  {
public:
   //--- default initialization method
   virtual void Init(string &symb[],ENUM_TIMEFRAMES &tfs[]) { }

   //--- default method for obtaining string value to display in the table cell
   virtual string GetValue(string symbol,ENUM_TIMEFRAMES tf) { return("-"); }

   //--- default method for obtaining color for table cell
   virtual color GetColor(string symbol,ENUM_TIMEFRAMES tf) { return(clrWhite); }
   
   //--- default method for obtaining row name
   virtual string GetName() { return("-"); }

   //--- default method for obtaining font for table cell
   virtual string GetFont(string symbol,ENUM_TIMEFRAMES tf) { return("Arial"); }
  };

It extends CObject, because only CObjects can be stored in a CList structure. It has five methods, which are almost empty. To be more precise, most of them only return default values. These methods are designed to be overriden when extending CRow. We don't need to override all of them, only the ones we want.

As an example let's create the simpliest possible panel component - current bid price component. It can be used in multi-currency mode to display current prices of various instruments.

To achieve this we create a CPriceRow class, which looks like this:

//+------------------------------------------------------------------+
//| CPriceRow class                                                  |
//+------------------------------------------------------------------+
class CPriceRow : public CRow
  {
public:
   //--- overrides default GetValue(..) method from CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetName() method from CRow
   virtual string    GetName();

  };
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method from CRow                  |
//+------------------------------------------------------------------+
string CPriceRow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   MqlTick tick;

//--- gets current price
   if(!SymbolInfoTick(symbol,tick)) return("-");

   return(DoubleToString(tick.bid,(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS)));
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method from CRow                     |
//+------------------------------------------------------------------+
string CPriceRow::GetName()
  {
   return("Price");
  }

The methods we chose to override here are GetValue and GetName. GetName simply returns the name for this row, which will be displayed in the first column of the panel. GetValue gets the latest tick on the specified symbol and returns the latest bid price. That is all we need.

This was quite simple. Let's do something different. We will now build a component, which shows the current RSI value.

The code is similiar to the previous one:

//+------------------------------------------------------------------+
//| CRSIRow class                                                    |
//+------------------------------------------------------------------+
class CRSIRow : public CRow
  {
private:
   int               rsiPeriod;        // RSI period
   string            symbols[];        // symbols array
   ENUM_TIMEFRAMES   timeframes[];     // timeframes array
   int               handles[];        // array of RSI handles

   //--- finds the indicator handle for a given symbol and timeframe
   int               GetHandle(string symbol,ENUM_TIMEFRAMES tf);

public:
   //--- constructor
                     CRSIRow(int period);

   //--- overrides default GetValue(..) method from CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetName() method from CRow
   virtual string    GetName();

   //--- overrides default Init(..) method from CRow
   virtual void      Init(string &symb[],ENUM_TIMEFRAMES &tfs[]);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRSIRow::CRSIRow(int period)
  {
   rsiPeriod=period;
  }
//+------------------------------------------------------------------+
//| Overrides default Init(..) method from CRow                      |
//+------------------------------------------------------------------+
void CRSIRow::Init(string &symb[],ENUM_TIMEFRAMES &tfs[])
  {
   int size=ArraySize(symb);
   
   ArrayResize(symbols,size);
   ArrayResize(timeframes,size);
   ArrayResize(handles,size);
   
//--- copies arrays contents into own arrays
   ArrayCopy(symbols,symb);
   ArrayCopy(timeframes,tfs);
  
//--- gets RSI handles for all used symbols or timeframes
   for(int i=0; i<ArraySize(symbols); i++)
      handles[i]=iRSI(symbols[i],timeframes[i],rsiPeriod,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method from CRow                  |
//+------------------------------------------------------------------+
string CRSIRow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   double value[1];

//--- gets RSI indicator handle
   int handle=GetHandle(symbol,tf);

   if(handle==INVALID_HANDLE) return("err");

//--- gets current RSI value
   if(CopyBuffer(handle,0,0,1,value)<0) return("-");

   return(DoubleToString(value[0],2));
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method from CRow                     |
//+------------------------------------------------------------------+
string CRSIRow::GetName()
  {
   return("RSI("+IntegerToString(rsiPeriod)+")");
  }
//+------------------------------------------------------------------+
//| finds the indicator handle for a given symbol and timeframe      |
//+------------------------------------------------------------------+
int CRSIRow::GetHandle(string symbol,ENUM_TIMEFRAMES tf)
  {
   for(int i=0; i<ArraySize(timeframes); i++)
      if(symbols[i]==symbol && timeframes[i]==tf)
         return(handles[i]);

   return(INVALID_HANDLE);
  }

We have a few new methods here. The constructor allows to supply the RSI period and stores it as a member variable. The Init method is used for creating RSI indicator handles. These handles are stored in the handles[] array. The GetValue method copies the last value from the RSI buffer and returns it. Private GetHandle method is used for finding proper indicator handle in the handles[] array. GetName is self-explanatory.

As we can see, building panel components is quite easy. In the same way we can create components for almost any custom condition. It does not have to be the indicator value. Below I present a custom condition based on SMA. It checks if current price is above the moving average and displays 'Yes' or 'No'.

//+------------------------------------------------------------------+
//| CPriceMARow class                                                |
//+------------------------------------------------------------------+
class CPriceMARow : public CRow
  {
private:
   int               maPeriod; // period of moving average
   int               maShift;  // shift of moving average
   ENUM_MA_METHOD    maType;   // SMA, EMA, SMMA or LWMA
   string            symbols[];        // symbols array
   ENUM_TIMEFRAMES   timeframes[];     // timeframes array
   int               handles[];        // array of MA handles

   //--- finds the indicator handle for a given symbol and timeframe
   int               GetHandle(string symbol,ENUM_TIMEFRAMES tf);

public:
   //--- constructor
                     CPriceMARow(ENUM_MA_METHOD type,int period,int shift);

   //--- overrides default GetValue(..) method of CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   // overrides default GetName() method CRow
   virtual string    GetName();

   //--- overrides default Init(..) method from CRow
   virtual void      Init(string &symb[],ENUM_TIMEFRAMES &tfs[]);
  };
//+------------------------------------------------------------------+
//| CPriceMARow class constructor                                    |
//+------------------------------------------------------------------+
CPriceMARow::CPriceMARow(ENUM_MA_METHOD type,int period,int shift)
  {
   maPeriod= period;
   maShift = shift;
   maType=type;
  }
//+------------------------------------------------------------------+
//| Overrides default Init(..) method from CRow                      |
//+------------------------------------------------------------------+
void CPriceMARow::Init(string &symb[],ENUM_TIMEFRAMES &tfs[])
  {
   int size=ArraySize(symb);
   
   ArrayResize(symbols,size);
   ArrayResize(timeframes,size);
   ArrayResize(handles,size);
   
//--- copies arrays contents into own arrays
   ArrayCopy(symbols,symb);
   ArrayCopy(timeframes,tfs);
  
//--- gets MA handles for all used symbols or timeframes
   for(int i=0; i<ArraySize(symbols); i++)
      handles[i]=iMA(symbols[i],timeframes[i],maPeriod,maShift,maType,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method of CRow                    |
//+------------------------------------------------------------------+
string CPriceMARow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   double value[1];
   MqlTick tick;

//--- obtains MA indicator handle
   int handle=GetHandle(symbol,tf);

   if(handle==INVALID_HANDLE) return("err");

//--- gets the last MA value
   if(CopyBuffer(handle,0,0,1,value)<0) return("-");
//--- gets the last price
   if(!SymbolInfoTick(symbol,tick)) return("-");

//--- checking the condition: price > MA
   if(tick.bid>value[0])
      return("Yes");
   else
      return("No");
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method of CRow                       |
//+------------------------------------------------------------------+
string CPriceMARow::GetName()
  {
   string name;

   switch(maType)
     {
      case MODE_SMA: name = "SMA"; break;
      case MODE_EMA: name = "EMA"; break;
      case MODE_SMMA: name = "SMMA"; break;
      case MODE_LWMA: name = "LWMA"; break;
     }

   return("Price>"+name+"("+IntegerToString(maPeriod)+")");
  }
//+------------------------------------------------------------------+
//| finds the indicator handle for a given symbol and timeframe      |
//+------------------------------------------------------------------+
int CPriceMARow::GetHandle(string symbol,ENUM_TIMEFRAMES tf)
  {
   for(int i=0; i<ArraySize(timeframes); i++)
      if(symbols[i]==symbol && timeframes[i]==tf)
         return(handles[i]);

   return(INVALID_HANDLE);
  }

The code is longer, because Moving Average has three parameters: period, shift and type. GetName is a little more complicated as it builds the name based on MA type and period. GetValue works almost in the same way as in case of CRSIRow, but instead of returning the indicator value it returns 'Yes' if price is above the SMA or 'No' if it is below.

The last example is a bit more complex. It is the CPriceChangeRow class, which shows the current bar's price change. It works in three modes:

  • Displaying arrows (green up or red down);
  • Displaying price change as a value (green or red);
  • Displaying price change as a percent (green or red).

The code looks as follows:

//+------------------------------------------------------------------+
//| CPriceChangeRow class                                            |
//+------------------------------------------------------------------+
class CPriceChangeRow : public CRow
  {
private:
   bool              percentChange;
   bool              useArrows;

public:
   //--- constructor
                     CPriceChangeRow(bool arrows,bool percent=false);

   //--- overrides default GetName() method from CRow
   virtual string    GetName();

   //--- overrides default GetFont() method from CRow
   virtual string    GetFont(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetValue(..) method from CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetColor(..) method from CRow
   virtual color     GetColor(string symbol,ENUM_TIMEFRAMES tf);

  };
//+------------------------------------------------------------------+
//| CPriceChangeRow class constructor                                |
//+------------------------------------------------------------------+
CPriceChangeRow::CPriceChangeRow(bool arrows,bool percent=false)
  {
   percentChange=percent;
   useArrows=arrows;
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method from CRow                     |
//+------------------------------------------------------------------+
 string CPriceChangeRow::GetName()
  {
   return("PriceChg");
  }
//+------------------------------------------------------------------+
//| Overrides default GetFont() method from CRow                     |
//+------------------------------------------------------------------+
string CPriceChangeRow::GetFont(string symbol,ENUM_TIMEFRAMES tf)
  {
//--- we use Wingdings font to draw arrows (up/down)
   if(useArrows)
      return("Wingdings");
   else
      return("Arial");
  }
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method from CRow                  |
//+------------------------------------------------------------------+
string CPriceChangeRow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   double close[1];
   double open[1];

//--- gets open and close of current bar
   if(CopyClose(symbol,tf,0, 1, close) < 0) return(" ");
   if(CopyOpen(symbol, tf, 0, 1, open) < 0) return(" ");

//--- current bar price change
   double change=close[0]-open[0];

   if(useArrows)
     {
      if(change > 0) return(CharToString(233)); // returns up arrow code
      if(change < 0) return(CharToString(234)); // returns down arrow code
      return(" ");
        }else{
      if(percentChange)
        {
         //--- calculates percent change
         return(DoubleToString(change/open[0]*100.0,3)+"%");
           }else{
         return(DoubleToString(change,(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS)));
        }
     }
  }
//+------------------------------------------------------------------+
//| Overrides default GetColor(..) method from CRow                  |
//+------------------------------------------------------------------+
color CPriceChangeRow::GetColor(string symbol,ENUM_TIMEFRAMES tf)
  {
   double close[1];
   double open[1];

//--- gets open and close of current bar
   if(CopyClose(symbol,tf,0, 1, close) < 0) return(clrWhite);
   if(CopyOpen(symbol, tf, 0, 1, open) < 0) return(clrWhite);

   if(close[0] > open[0]) return(clrLime);
   if(close[0] < open[0]) return(clrRed);
   return(clrWhite);
  }

The constructor has two parameters. First one decides whether to show arrows. If it's true, the second parameter is discarded. If it's false, the second parameter decides whether to show percent changes or just price changes.

For this class I decided to override four methods of CRow: GetName, GetValue, GetColor and GetFont. GetName is the simpliest and just returns the name. GetFont is used, because it gives the possibility to display arrows or other characters from Wingdings font. GetColor returns lime color when the price rises and red when it falls. White color is returned when it stays in place or in case of errors. GetValue gets the open and close prices of the last bar, calculates the difference and returns it. In the arrow mode it returns Wingdings character codes of up and down arrows.


5. How to use the whole thing

In order to use the panel we need to create a new indicator. Let's call it TableSample.

The events we need to handle are:

We also need a pointer to CTable object, which will be created dynamically in OnInit(). First of all, we must decide, which mode we will use (multi-timeframe or multi-currency). The code sample below shows the multi-currency mode, but eveything that is needed for the multi-timeframe mode is also here in comments. For multi-currency mode we need to create an array of symbols and pass it to the CTable constructor. For multi-timeframe mode we would create an array of timeframes and pass it to the second CTable constructor.

After that we have to create all the necessary components and add them to the panel using the AddRow method. Optionally, panel parameters may be adjusted. After all, we need to draw the panel for the first time, thus we call Update at the end of OnInit(). OnDeinit is simple. The only thing it does is deleting the CTable object, what causes the CTable destructor to be called.

OnCalculate(...) and OnChartEvent(...) are identical. They only call the Update method. OnChartEvent(...) is only necessary if the panel works in multi-currency mode. In this mode it handles events raised by SpyAgents. In multi-timeframe mode only OnCalculate(...) is needed, because we have to monitor only the current chart's symbol.

//+------------------------------------------------------------------+
//|                                                  TableSample.mq5 |
//|                                                 Marcin Konieczny |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Marcin Konieczny"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0

#include <Table.mqh>
#include <PriceRow.mqh>
#include <PriceChangeRow.mqh>
#include <RSIRow.mqh>
#include <PriceMARow.mqh>

CTable *table; // pointer to CTable object
//+------------------------------------------------------------------+
//| Indicator initialization function                                |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- timeframes used in table (in multi-timeframe mode)
   ENUM_TIMEFRAMES timeframes[4]={PERIOD_M1,PERIOD_H1,PERIOD_D1,PERIOD_W1};

//--- symbols used in table (in multi-currency mode)
   string symbols[4]={"EURUSD","GBPUSD","USDJPY","AUDCHF" };
//-- CTable object creation 
//   table = new CTable(timeframes); // multi-timeframe mode
   table=new CTable(symbols); // multi-currency mode

//--- adding rows to the table
   table.AddRow(new CPriceRow());               // shows current price
   table.AddRow(new CPriceChangeRow(false));     // shows change of price in the last bar
   table.AddRow(new CPriceChangeRow(false,true)); // shows percent change of price in the last bar
   table.AddRow(new CPriceChangeRow(true));      // shows change of price as arrows
   table.AddRow(new CRSIRow(14));                // shows RSI(14)
   table.AddRow(new CRSIRow(10));                // shows RSI(10)
   table.AddRow(new CPriceMARow(MODE_SMA,20,0));  // shows if SMA(20) > current price

//--- setting table parameters
   table.SetFont("Arial",10,clrYellow);  // font, size, color
   table.SetCellSize(60, 20);           // width, height
   table.SetDistance(10, 10);           // distance from upper right chart corner

   table.Update(); // forces table to redraw

   return(0);
  }
//+------------------------------------------------------------------+
//| Indicator deinitialization function                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- calls table destructor and frees memory
   delete(table);
  }
//+------------------------------------------------------------------+
//| Indicator iteration function                                     |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
//--- update table: recalculate/repaint
   table.Update();
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnChartEvent handler                                             |
//| Handles CHARTEVENT_CUSTOM events sent by SpyAgent indicators     |
//| nedeed only in multi-currency mode!                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   table.Update(); // update table: recalculate/repaint
  }
//+------------------------------------------------------------------+

After attaching this indicator to the chart, it starts updating and we can finally see the panel working.


6. Installation

All files need to be compiled. SpyAgent and TableSample are indicators and should be copied to terminal_data_folder\MQL5\Indicators. The remaining files are include files and should be placed inside terminal_data_folder\MQL5\Include. To run the panel, attach the TableSample indicator to any chart. There is no need to attach SpyAgent's. They will be launched automatically.


Conclusion

The article provides an object-oriented implementation of multi-timeframe and multi-currency panel for MetaTrader 5. It shows how to achieve a design, which is easily extensible and allows to build customized panels with little effort.

All the code presented in this article can be downloaded below.


Last comments | Go to discussion (3)
[Deleted] | 23 May 2012 at 22:25

Thank you very much.

Can you add UML chart please. 

[Deleted] | 25 May 2012 at 10:11
class CRow  Is this an abstract class?
seu madruga
seu madruga | 22 May 2020 at 20:07

Hello guys,


this one dont compiles anymore..

Custom Graphical Controls. Part 3. Forms Custom Graphical Controls. Part 3. Forms
This is the last of the three articles devoted to graphical controls. It covers the creation of the main graphical interface component - the form - and its use in combination with other controls. In addition to the form classes, CFrame, CButton, CLabel classes have been added to the control library.
Interview with Andrea Zani (ATC 2011) Interview with Andrea Zani (ATC 2011)
On the eleventh week of the Automated Trading Championship, Andrea Zani (sbraer) got featured very close to the top five of the competition. It is on the sixth place with about 47,000 USD now. Andrea's Expert Advisor AZXY has made only one losing deal, which was at the very beginning of the Championship. Since then, its equity curve has been steadily growing.
The Role of Statistical Distributions in Trader's Work The Role of Statistical Distributions in Trader's Work
This article is a logical continuation of my article Statistical Probability Distributions in MQL5 which set forth the classes for working with some theoretical statistical distributions. Now that we have a theoretical base, I suggest that we should directly proceed to real data sets and try to make some informational use of this base.
Interview with Li Fang (ATC 2011) Interview with Li Fang (ATC 2011)
On the seventh week of the Championship, Li Fang's Expert Advisor (lf8749) set a new record - it earned over $100,000 in 10 trades. This successful series helped the Expert Advisor to stay at the very top of the Automated Trading Championship 2011 rating for two weeks. In this interview we tried to find out the secret of Li Fang's success.