Русский Español Português
preview
Visualizing Strategies in MQL5: Laying Out Optimization Results Across Criterion Charts

Visualizing Strategies in MQL5: Laying Out Optimization Results Across Criterion Charts

MetaTrader 5Examples |
234 2
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

mql5.com contains so much information that every time you scroll through article catalogues or reference information, or a textbook, you will definitely find something new and interesting for yourself.

This is the case this time. I came across a simple and at first glance uncomplicated article, which briefly describes the strategy tester. Everything seems to be simple and has been known for a long time, but... But the last part of the article intrigued me. It suggests simply connecting a small code to the Expert Advisor, adding several standard handlers to it, and... and the usual optimizer of MetaTrader 5 platform strategy tester turns into a visual one. Hm... That’s interesting.

I began to study and sort things out. As a result, an idea was born to slightly improve the appearance and expand a feature of viewing the optimization results.

Let's do this: the Expert Advisor will open a new window with five tabs. The first one will have a chart of all passes, where each new pass will be displayed with a balance line. The other four tabs will also have charts, but they will be available after optimization is complete. Each of these tabs will display data on the top three passes by one of four optimization criteria. And on each tab there will be two tables: with results of the optimization pass and settings of the Expert Advisor for this pass:

  1. Optimization tab:

    1. a table of optimization results for the next pass,
    2. a table of EA's input parameters for this pass,
    3. balance chart of the current completed optimization pass,
    4. Replay button for replaying the optimization performed.

  2. Sharpe Ratio tab:

    1. a table of optimization results for the selected pass (one of the top three by the Sharpe Coefficient), 
    2. a table of Expert Advisor’s input parameters for the selected pass (one of the top three by the Sharpe Coefficient), 
    3. balance charts of the three best optimization passes by the Sharpe Coefficient.
    4. A toggle (three-position) for selecting one of the three best optimization results by the Sharpe Coefficient.

  3. Net Profit tab:

    1. a table of optimization results for the selected pass (one of the top three by Total Profit),
    2. a table of Expert Advisor’s input parameters for the selected pass (one of the top three by Total Profit), 
    3. balance charts of the three best optimization passes by Total Profit.
    4. A toggle (three-position) for selecting one of the three best optimization results by Total Profit.

  4. Profit Factor tab:

    1. a table of optimization results for the selected pass (one of the top three by Profitability), 
    2. a table of Expert Advisor’s input parameters for the selected pass (one of the top three by Profitability),  
    3. balance charts of the three best optimization passes by Profitability, 
    4. A toggle (three-position) for selecting one of the three best optimization results by Profitability.

  5. Recovery Factor tab:

    1. a table of optimization results for the selected pass (one of the top three by Recovery Factor),  
    2. a table of Expert Advisor’s input parameters for the selected pass (one of the top three by Recovery Factor),  
    3. balance charts of the three best optimization passes by Recovery Factor,  
    4. A toggle (three-position) for selecting one of the three best optimization results by Recovery Factor.  

To implement a set of tabs, make classes of controls, from which make up the Tab Control. Skip the process of creating controls in this article by simply offering a ready-made class file. In following articles, we will return to description of such classes for creating some controls that may be useful in the future.

To display information about pass parameters, we want table classes to be taken in a ready-made form from "SQLite capabilities in MQL5: Example of a dashboard with trading statistics by symbols and magic numbers” article and slightly refine table classes to create tables and output text to their cells more conveniently.

To implement the idea, take codes for working with optimization frameworks attached to the above mentioned article, and based on them make our own classes, trying to preserve the concept as much as possible. Since the article does not describe the process of working with frames and the Expert Advisor working in frame mode, let's try to understand this system here.


How It Works

Let's turn to the MQL5 Tutorial, and see what it says about functioning of strategy tester and its optimizer:

... A particularly important function of the tester is multithreaded optimization, which can be performed using local and distributed (networked) agent programs, including in the MQL5 Cloud Network. A single test run (with specific EA’s input parameters), started manually by the user, or one of the many runs caused by optimization (when the parameter values are iterated within set ranges) is performed in a separate agent program. Technically— it is metatester64.exe file, and copies of its processes are available in the Windows Task Manager during testing and optimization. This is why the tester is multithreaded.

The terminal is the Execution Manager that distributes tasks to local and remote agents. It launches local agents itself, if necessary. During optimization, several agents are launched by default — their number corresponds to the number of processor cores. After completing the next task of testing the Expert Advisor with specified parameters, the agent returns the results to the terminal.

Each agent creates its own trading and software environment. All agents are isolated from each other and from the client terminal.

What can be clear from the description: each instance of the EA being tested is launched on its own testing agent, and each pass — its final data — is sent from the agent to the terminal.

There is a set of handlers for data exchange between the terminal and agents:

  • OnTesterInit() — it is called in Expert Advisors when TesterInit event occurs to perform necessary actions before starting optimization in the strategy tester.
  • OnTester() — it is called in Expert Advisors when Tester event occurs to perform necessary actions upon completion of testing.
  • OnTesterPass() — it is called in Expert Advisors when TesterPass to process a new data frame during Expert Advisor optimization.
  • OnTesterDeinit() — it is called in Expert Advisors when TesterDeinit event occurs to perform necessary actions upon completion of EA optimization.

If the EA has any of the handlers OnTesterInit(), OnTesterDeinit() (these two handlers always work in pair - you can't have just one of them), OnTesterPass(), then the expert will be launched in a separate terminal window in a special frame mode:

To manage the optimization process and transfer arbitrary application results from agents to the terminal (in addition to trade indicators), there are 3 special events in MQL5: OnTesterInit, OnTesterDeinit, OnTesterPass. By describing the handlers in the code for them, the programmer will be able to perform the actions he needs before starting optimization, after optimization is completed, and at completion of each of the individual optimization passes.

All handlers are optional. Optimization runs without them, as well. It should also be understood that all 3 events work only during optimization, but not in a single test.

The Expert Advisor with these handlers is automatically loaded on a separate terminal chart with the symbol and period as specified in the tester. This copy of the Expert Advisor does not trade, but performs only service actions. All other event handlers do not run in it, in particular, OnInit, OnDeinit, onTick.

During optimization, only one instance of the Expert Advisor works in the terminal and, if necessary, accepts incoming frames. But to make it clear once again that such an instance of the Expert Advisor is launched only if one of the three described event handlers is present in its code.

After each individual pass of the optimizer is completed, the OnTester() event is generated in the EA’s instance running on the agent. From the handler of this event, you can send data about a pass to the Expert Advisor that runs on a separate chart in a special frame mode. The data packet about the completed pass sent to the Expert Advisor on the chart is called a frame. It contains information about the pass number, values of EA's input variables with which the pass was started, and results of this pass.

All this data is sent to the Expert Advisor, and a TesterPass event is generated in it, being processed in OnTesterPass() handler, where we can read the pass data and perform any actions (in this case, for example, draw a balance chart of this pass and perform other service actions).

To send data about the pass from the agent to the Expert Advisor on the chart in the terminal, one should use the FrameAdd() function. The current frame (completed pass) will be sent from the agent to the Expert Advisor and will already be handled there in the OnTesterPass() handler.

As you can see, some functions work on the agent in the EA instance running in it, and some work in the Expert Advisor on the terminal chart running in frame mode. But all of them, of course, must be described inside the EA code.

As a result, the sequence of the EA's operation and our actions when transferring data between the agent and the terminal is as follows:

  • In the OnTesterInit handler (an instance of the Expert Advisor on the chart in the terminal), it is necessary to prepare all graphical constructions — a separate chart on which the EA is running in frame mode, and the contents of this chart: a chart of the balance chart, tables with parameters and results, a tab control object and buttons for selecting actions on tabs;

  • In the OnTester handler (an Expert Advisor instance on the agent), it is necessary to collect all the information about the completed pass — write the balance result of each closing transaction into an array, receive and write the received results of this pass into an array, and send all this data to the EA using FrameAdd();

  • In the OnTesterPass handler (an Expert Advisor instance on the chart in the terminal), we receive the next frame sent from the agent using FrameAdd(), read its data and draw a balance graph on the chart, create a frame object and save it to an array for subsequent sorting and selection according to optimization criteria;

  • OnTesterDeinit and OnChartEvent handlers (an Expert Advisor instance on the chart in the terminal) work with optimization data after its completion — a repeated generation of the optimization process, showing the best results on certain optimization criteria.


Make Classes For Our Tasks

To create the Tab Control, a file with a set of controls Controls.mqh was created. The file is attached at the end of the article, and it should be placed directly in the folder where we will write the test EA, for example, in the terminal directory \MQL5\Experts\FrameViewer\Controls.mqh.

We will not consider each created class of each control here. Give just a brief overview.

In total, ten classes were implemented for eight independent controls:

# Class
Parent class
Description
 Assignment
 1  CBaseCanvas  CObject Drawing base class The base canvas. It contains methods for setting and changing the size and position, hiding and displaying
 2  CPanel  CBaseCanvas Panel class It contains methods for setting and changing colors and mouse event handlers. It enables to attach child controls
 3  CLabel  CPanel Text label class It prints a text on the canvas in the set coordinates
 4  CButton  CLabel Simple button class A regular button with an unfixed state. It reacts to hovering and mouse clicks by changing the color
 5  CButtonTriggered  CButton Two-way button class A button with two states: On/Off It reacts to hovering, mouse clicks and a change of the state by changing the color
 6  CTabButton  CButtonTriggered Tab button class A two-way button with a missing border at the junction with the tab field
 7  CButtonSwitch  CPanel Toggle button class A panel with two or more two-way buttons, where only one can have the On state. It allows you to add new buttons to existing ones.
 8  CTabWorkArea  CObject Workspace class of the tab An object that has two basic drawing classes — for the background and foreground.
 9  CTab  CPanel Tab object class A panel with a button and a field. The workspace is located on the tab field, where the drawing takes place.
 10  CTabControl  CPanel The class of the tab control object A panel that allows you to add and manage tab objects.

After the control object has been successfully created, its Create() method must be called for each of the objects, specifying its coordinates and dimensions. After that, the element is ready to work with it.

The control element, which includes implemented event handlers, sends user events to the control program chart, which can be used to determine what has been done in the object:

#
Class
Event
ID
lparam
dparam
 sparam
 1 CButton Click on the object (ushort)CHARTEVENT_CLICK X-coordinate of the cursor Y-coordinate of the cursor Button object name
 2 CButtonTriggered Click on the object (ushort)CHARTEVENT_CLICK X-coordinate of the cursor Y-coordinate of the cursor Button object name
 3 CTabButton Click on the object (ushort)CHARTEVENT_CLICK X-coordinate of the cursor Y-coordinate of the cursor Button object name
 4 CButtonSwitch Click on object button (ushort)CHARTEVENT_CLICK Button ID 0 Toggle object name

The table shows that to simplify the code, there is no reference from the Tab Control to the chart of the user event program. If the program requires reaction to tab switching, then you can determine the event by clicking on the TabButton. By the button name, you can find out the tab number, or request the index of the selected tab from TabControl object, etc.

In any case, in the future we will analyze such classes in detail when creating various controls that are useful for use in our programs.

Now we must slightly refine the table class presented in the article, which should be downloaded (Dashboard.mqh file), copy only the table class code from the file (lines 12 - 285) and save the copied code in \MQL5\Experts\FrameViewer\ folder in Table.mqh file.

Add the class so that working with tables and tabular data is a little more convenient.

Connect to the file a file of the class of a dynamic array of pointers to instances of the CObject class and its descendants CArrayObj and a file of the class for simplified creation of custom drawings CCanvas:

//+------------------------------------------------------------------+
//|                                                        Table.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"

#include <Arrays\ArrayObj.mqh>
#include <Canvas\Canvas.mqh>

To the private section of the table cell class add new variables to store the width, height, and color of the text in the cell:

//+------------------------------------------------------------------+
//| Table cell class                                                 |
//+------------------------------------------------------------------+
class CTableCell : public CObject
  {
private:
   int               m_row;                     // Row
   int               m_col;                     // Column
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
   int               m_w;                       // Width
   int               m_h;                       // Height
   string            m_text;                    // Text in the cell
   color             m_fore_color;              // Text color in the cell
public:

In the public section, add methods for reading and setting new properties and a method that prints the text written in the cell to the specified canvas object:

public:
//--- Methods for setting values
   void              SetRow(const uint row)     { this.m_row=(int)row;  }
   void              SetColumn(const uint col)  { this.m_col=(int)col;  }
   void              SetX(const uint x)         { this.m_x=(int)x;      }
   void              SetY(const uint y)         { this.m_y=(int)y;      }
   void              SetXY(const uint x,const uint y)
                       {
                        this.m_x=(int)x;
                        this.m_y=(int)y;
                       }
   void              SetWidth(const uint w)     { this.m_w=(int)w;      }
   void              SetHeight(const uint h)    { this.m_h=(int)h;      }
   void              SetSize(const uint w,const uint h)
                       {
                        this.m_w=(int)w;
                        this.m_h=(int)h;
                       }
   void              SetText(const string text) { this.m_text=text;     }
   
//--- Methods for getting values
   int               Row(void)            const { return this.m_row;    }
   int               Column(void)         const { return this.m_col;    }
   int               X(void)              const { return this.m_x;      }
   int               Y(void)              const { return this.m_y;      }
   int               Width(void)          const { return this.m_w;      }
   int               Height(void)         const { return this.m_h;      }
   string            Text(void)           const { return this.m_text;   }
   
//--- Prints the text written in cell properties to the canvas, the pointer to which is passed to the method
   void              TextOut(CCanvas *canvas, const int x_shift, const int y_shift, const color bg_color=clrNONE, const uint flags=0, const uint alignment=0)
                       {
                        if(canvas==NULL)
                           return;
                        //--- Remember current font flags
                        uint flags_prev=canvas.FontFlagsGet();
                        //--- Set background color
                        uint clr=(bg_color==clrNONE ? 0x00FFFFFF : ::ColorToARGB(bg_color));
                        //--- Fill in the cell with the set background color (erase the previous label)
                        canvas.FillRectangle(this.m_x+1, this.m_y+1, this.m_x+this.m_w-1, this.m_y+this.m_h-1, clr);
                        //--- Set font flags
                        canvas.FontFlagsSet(flags);
                        //--- Print text in the cell
                        canvas.TextOut(this.m_x+x_shift, this.m_y+y_shift, this.m_text, ::ColorToARGB(this.m_fore_color), alignment);
                        //--- Return previously memorized font flags and update canvas
                        canvas.FontFlagsSet(flags_prev);
                        canvas.Update(false);
                       }
   
//--- A virtual method for comparing two objects

At the end of class listing, implement a new table control class:

//+------------------------------------------------------------------+
//| Table control class                                              |
//+------------------------------------------------------------------+
class CTableDataControl : public CTableData
  {
protected:
   uchar             m_alpha;  
   color             m_fore_color;
   
//--- Converts RGB to color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Writes RGB component values to variables
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Returns color component (1) Red, (2) Green, (3) Blue
   double            GetR(const color clr)      { return clr&0xff ;                    }
   double            GetG(const color clr)      { return(clr>>8)&0xff;                 }
   double            GetB(const color clr)      { return(clr>>16)&0xff;                }

//--- Returns a new color
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);
  
public:
//--- Returns a pointer to itself
   CTableDataControl*Get(void)                                 { return &this;         }
   
//--- (1) Sets, (2) returns transparency
   void              SetAlpha(const uchar alpha)               { this.m_alpha=alpha;   }
   uchar             Alpha(void)                         const { return this.m_alpha;  }
   
//--- Draws (1) a background grid, (2) with automatic cell size
   void              DrawGrid(CCanvas *canvas,const int x,const int y,const uint header_h,const uint rows,const uint columns,const uint row_size,const uint col_size,
                              const color line_color=clrNONE,bool alternating_color=true);
   void              DrawGridAutoFill(CCanvas *canvas,const uint border,const uint header_h,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true);
   
//--- Prints (1) a text message, (2) a filled rectangle at the specified coordinates
   void              DrawText(CCanvas *canvas,const string text,const int x,const int y,const color clr=clrNONE,const uint align=0,const int width=WRONG_VALUE,const int height=WRONG_VALUE);
   void              DrawRectangleFill(CCanvas *canvas,const int x,const int y,const int width,const int height,const color clr,const uchar alpha);
                       
   
//--- Constructors/Destructor
                     CTableDataControl (const uint id) : CTableData(id), m_fore_color(clrDimGray), m_alpha(255) {}
                     CTableDataControl (void) : m_alpha(255) {}
                    ~CTableDataControl (void) {}
  };
//+------------------------------------------------------------------+
//| Draws background grid                                             |
//+------------------------------------------------------------------+
void CTableDataControl::DrawGrid(CCanvas *canvas,const int x,const int y,const uint header_h,const uint rows,const uint columns,const uint row_size,const uint col_size,
                                 const color line_color=clrNONE,bool alternating_color=true)
  {
//--- Clear all lists of the tabular data object (delete cells from rows and all rows)
   this.Clear();
//--- Row height cannot be less than 2
   int row_h=int(row_size<2 ? 2 : row_size);
//--- Row width cannot be less than 2
   int col_w=int(col_size<2 ? 2 : col_size);
   
//--- Left coordinate (X1) of the table
   int x1=x;
//--- Calculate X2 coordinate (on the right) depending on the number of columns and their width
   int x2=x1+col_w*int(columns>0 ? columns : 1);
//--- Y1 coordinate is located under the panel header area
   int y1=(int)header_h+y;
   
//--- Calculate Y2 coordinate (from below) depending on the number of rows and their height
   int y2=y1+row_h*int(rows>0 ? rows : 1);
//--- Set coordinates of the table
   this.SetCoords(x1,y1-header_h,x2,y2-header_h);
   
//--- Get color of grid lines of table, either by default or passed to method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- Draw table border
   canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
//--- In a loop by table rows
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate Y coordinate of next horizontal grid line (Y coordinate of next row of table)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" row colors is passed and the row is even
      if(alternating_color && i%2==0)
        {
         //--- lighten background color of table and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw horizontal grid line of table
      canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to list of rows of tabular data object
      //--- (if failed to add object, delete created object)
      if(!this.AddRow(row_obj))
         delete row_obj;
      //--- Set Y coordinate in created row object, given offset from panel header
      row_obj.SetY(row_y-header_h);
     }
     
//--- In loop by table columns
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate X coordinate of next vertical grid line (X coordinate of next table column)
      int col_x=x1+col_w*i;
      //--- If grid line has gone beyond panel, break cycle
      if(x1==1 && col_x>=x1+canvas.Width()-2)
         break;
      //--- Draw vertical grid line of table
      canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from tabular data object
      int total=this.RowsTotal();
      //--- In loop through table rows
      for(int j=0;j<total;j++)
        {
         //--- get next row
         CTableRow *row=this.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create new table cell
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add created cell to row
         //--- (if failed to add object, delete created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In created cell object, set its X coordinate and Y coordinate from row object.
         cell.SetXY(col_x,row.Y());
         cell.SetSize(col_w, row_h);
        }
     }
//--- Update canvas without redrawing chart
   canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Draws background grid with automatic cell size                   |
//+------------------------------------------------------------------+
void CTableDataControl::DrawGridAutoFill(CCanvas *canvas,const uint border,const uint header_h,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true)
  {
//--- X1 coordinate (left) of table
   int x1=(int)border;
//--- X2 coordinate (right) of table
   int x2=canvas.Width()-(int)border-1;
//--- Y1 coordinate (top) of table
   int y1=int(header_h+border-1);
//--- Y2 coordinate (lower) of table
   int y2=canvas.Height()-(int)border-1;
//--- Set coordinates of table
   this.SetCoords(x1,y1,x2,y2);

//--- Get color of grid lines of table, either by default or passed to method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If indentation from edge of panel is greater than zero, draw border of table
//--- otherwise, panel border acts as table border
   if(border>0)
      canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));

//--- Height of entire table grid
   int greed_h=y2-y1;
//--- Calculate row height depending on table height and number of rows
   int row_h=(int)::round((double)greed_h/(double)rows);
//--- In loop through number of rows
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate Y coordinate of next horizontal grid line (Y coordinate of next row of table)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" row colors is passed and the row is even
      if(alternating_color && i%2==0)
        {
         //--- lighten background color of table and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw horizontal grid line of table
      canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to list of rows of tabular data object
      //--- (if failed to add object, delete created object)
      if(!this.AddRow(row_obj))
         delete row_obj;
      //--- Set Y coordinate in created row object, given offset from panel header
      row_obj.SetY(row_y-header_h);
     }
     
//--- Width of table grid
   int greed_w=x2-x1;
//--- Calculate column width depending on table width and number of columns
   int col_w=(int)::round((double)greed_w/(double)columns);
//--- In loop by table columns
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate X coordinate of next vertical grid line (X coordinate of next table column)
      int col_x=x1+col_w*i;
      //--- If this is not the very first vertical line, draw it
      //--- (the first vertical line is either table border or panel border)
      if(i>0)
         canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from tabular data object
      int total=this.RowsTotal();
      //--- In loop through table rows
      for(int j=0;j<total;j++)
        {
         //--- get next row
         CTableRow *row=this.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create new table cell
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add created cell to row
         //--- (if failed to add object, delete created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In created cell object, set its X coordinate and Y coordinate from row object.
         cell.SetXY(col_x,row.Y());
         cell.SetSize(col_w, row_h);
        }
     }
//--- Update canvas without redrawing chart
   canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Returns color with new color component                           |
//+------------------------------------------------------------------+
color CTableDataControl::NewColor(color base_color, int shift_red, int shift_green, int shift_blue)
  {
   double clR=0, clG=0, clB=0;
   this.ColorToRGB(base_color,clR,clG,clB);
   double clRn=(clR+shift_red  < 0 ? 0 : clR+shift_red  > 255 ? 255 : clR+shift_red);
   double clGn=(clG+shift_green< 0 ? 0 : clG+shift_green> 255 ? 255 : clG+shift_green);
   double clBn=(clB+shift_blue < 0 ? 0 : clB+shift_blue > 255 ? 255 : clB+shift_blue);
   return this.RGBToColor(clRn,clGn,clBn);
  }
//+------------------------------------------------------------------+
//| Converts RGB to color                                          |
//+------------------------------------------------------------------+
color CTableDataControl::RGBToColor(const double r,const double g,const double b) const
  {
   int int_r=(int)::round(r);
   int int_g=(int)::round(g);
   int int_b=(int)::round(b);
   int clr=0;
   clr=int_b;
   clr<<=8;
   clr|=int_g;
   clr<<=8;
   clr|=int_r;
//---
   return (color)clr;
  }
//+------------------------------------------------------------------+
//| Retrieving RGB component values                                  |
//+------------------------------------------------------------------+
void CTableDataControl::ColorToRGB(const color clr,double &r,double &g,double &b)
  {
   r=GetR(clr);
   g=GetG(clr);
   b=GetB(clr);
  }
//+------------------------------------------------------------------+
//| Prints text message to specified coordinates                     |
//+------------------------------------------------------------------+
void CTableDataControl::DrawText(CCanvas *canvas,const string text,const int x,const int y,const color clr=clrNONE,const uint align=0,const int width=WRONG_VALUE,const int height=WRONG_VALUE)
  {
//--- Declare variables to record width and height of text in them
   int w=width;
   int h=height;
//--- If width and height of text passed to method have zero values,
//--- entire canvas space is completely erased with transparent color
   if(width==0 && height==0)
      canvas.Erase(0x00FFFFFF);
//--- Otherwise
   else
     {
      //--- If passed width and height have default values (-1), get its width and height from text
      if(width==WRONG_VALUE && height==WRONG_VALUE)
         canvas.TextSize(text,w,h);
      //--- otherwise,
      else
        {
         //--- if width passed to method has default value (-1), get width from text, or
         //--- if width passed to method has value greater than zero, use width passed to method, or
         //--- if width passed to method has zero value, use value 1 for width
         w=(width ==WRONG_VALUE ? canvas.TextWidth(text)  : width>0  ? width  : 1);
         //--- if height passed to method has default value (-1), get height from text, or
         //--- if height passed to method has value greater than zero, use height passed to method, or
         //--- if height passed to method has zero value, use value 1 for height
         h=(height==WRONG_VALUE ? canvas.TextHeight(text) : height>0 ? height : 1);
        }
      //--- Fill space according to specified coordinates and by resulting width and height with transparent color (erase previous entry)
      canvas.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
     }
//--- Print text in place cleared of previous text and update workspace without redrawing screen
   canvas.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr),align);
   canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Prints filled rectangle to specified coordinates                 |
//+------------------------------------------------------------------+
void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const int x,const int y,const int width,const int height,const color clr,const uchar alpha)
  {
   canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha));
   canvas.Update();
  }
//+------------------------------------------------------------------+

This class contains the methods which principle was described in "Making a dashboard to display data in indicators and EAs" article in information panel description section. In the mentioned article, methods belonged to the panel object. Here they are placed in a separate class inherited from the table class.

All the tabular data objects here will have a CTableDataControl class type, a table control object, which enables to quickly control tables.

Let's see what we were asked to download and connect to the Expert Advisor in that long-ago article:

And the last “highlight of the performance” is working with optimization results! Previously, a trader had to prepare data to handle the results , upload it somewhere and process it somewhere else, but now this can be done "without leaving the cash register" — during the optimization itself. To demonstrate this feature, we need several include files in which the simplest examples of such processing were implemented.

Upload the files with MQH extension attached to the article to MQL5\Include folder. Take any EA and insert this block at the end:

//--- connect code to work with optimization results
#include <FrameGenerator.mqh>
//--- frame generator
CFrameGenerator fg;
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- here insert your own function to calculate optimization criterion
   double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT));
   TesterCritetia=TesterStatistics(STAT_PROFIT)>0?TesterCritetia:(-TesterCritetia);
//--- call at each end of testing and pass optimization criterion as parameter
   fg.OnTester(TesterCritetia);
//---
   return(TesterCritetia);
  }
//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
void OnTesterInit()
  {
//--- prepare chart for displaying balance charts
   fg.OnTesterInit(3); //parameter sets number of balance lines on chart
  }
//+------------------------------------------------------------------+
//| TesterPass function                                              |
//+------------------------------------------------------------------+
void OnTesterPass()
  {
//--- handle test results and display graphics
   fg.OnTesterPass();
  }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
//--- completing optimization
   fg.OnTesterDeinit();
  }
//+------------------------------------------------------------------+
//|  Event handling on chart                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   //--- starts playback of frames upon completion of optimization when clicking on header
   fg.OnChartEvent(id,lparam,dparam,sparam,100); // 100 - this is pause in ms between frames
  }
//+------------------------------------------------------------------+

As an example, a standard-supplied EA Moving Averages.mq5 was taken. Insert the code and save Expert Advisor with the following name Moving Averages With Frames.mq5. Compile and run optimization.

Go to article end and look at attached files. There are four files with the *.mqh extension there. Upload them and sort it out:

  • specialchart.mqh (7.61 KB) — a class of a special chart on which balance lines of each tester pass and the balance lines when reproducing the completed optimization process, are drawn;

  • colorprogressbar.mqh (4.86 KB) — a progress bar class that displays the optimization process, getting filled with colored columns during optimization. Green is for a profitable series, red is for a loss one, located at the bottom of a special chart;

  • simpletable.mqh (10.74 KB) — a class of a simple table that displays data of each optimization pass — the result obtained and the values of EA's configuration parameters, with which the EA was launched on this pass. Two tables are located to the left of charts of a special chart;

  • framegenerator.mqh (14.88 KB) — a class for exchanging data between the testing agent and the terminal and displaying information on a special chart. It is the main class for implementing visual optimization.

Based on the knowledge gained, we decide to make the following: (1) a progress bar class, (2) a special chart class, and (3) a frame viewer class. We already have a class of tables (4), it was uploaded to the folder of the future Expert Advisor and slightly modified.

Implementing another small class, a frame class (5), is required. What is the purpose of it? We will select and display charts of top three passes for each of the four optimization criteria — the Sharpe Ratio, Total Profit, Profitability, and Recovery Factor. It will be convenient to do this if we have a list of objects created based on the class of a dynamic array of pointers to instances of CObject class and its descendants of the Standard Library. It is sufficient to sort the list by the desired criterion, and all the objects in the list will be sorted by the value of the property of the selected criterion. An object with the maximum parameter value will be at the end of the list. It remains to find two objects that have a property value less than that of the previous found object. And methods for such a search are already all implemented in the mentioned class.

Progress bar class, special chart class, and frame viewer class are created based on the codes downloaded from the article — we just look at how it's done there, and use this foundation to implement our own classes, correcting, removing unnecessary things, and adding something necessary. Look at the resulting codes, and if desired, they can be compared with those taken from the old article — an archive with old files will be attached at the end of this article.

We will write all classes in a single file. Create it (if we have created it yet) in \MQL5\Experts\FrameViewer\FrameViewer.mqh and start filling it in.

Connect files of required classes and libraries to the created file and define some macro substitutions:

//+------------------------------------------------------------------+
//|                                                  FrameViewer.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"

#include "Controls.mqh"                               // Classes of controls
#include "Table.mqh"                                  // Table class
#include <Arrays\ArrayDouble.mqh>                     // Array of physical data

#define  CELL_W               128                     // Width of table cells
#define  CELL_H               19                      // Height of table cell
#define  BUTT_RES_W           CELL_W+30               // Width of optimization result selection button
#define  DATA_COUNT           8                       // Amount of data
#define  FRAME_ID             1                       // Frame ID
#define  TABLE_OPT_STAT_ID    1                       // ID of statistics table on optimization tab      
#define  TABLE_OPT_STAT_ID    2                       // ID of statistics table on optimization tab      

Almost every graphic object that will be used for drawing has several CCanvas objects. One can serve as a substrate on which two more are located: background image is drawn on the first one, and the second one shows what should be drawn on top of the background. For objects which methods are intended for drawing, a pointer to the desired canvas object is passed to these methods, on which the method will draw.

Since there is a lot of class code, and each class and its methods are fully commented on, we will not describe everything in detail and step by step here. Simply look at codes of classes and methods, for which we will give a brief overview of the presented code.

So, progress bar class:

//+------------------------------------------------------------------+
//|  Progress bar class that draws in two colors                     |
//+------------------------------------------------------------------+
class CColorProgressBar :public CObject
  {
private:
   CCanvas          *m_background;                    // Pointer to CCanvas class object for drawing on background
   CCanvas          *m_foreground;                    // Pointer to CCanvas class object for drawing on foreground
   CRect             m_bound;                         // Coordinates and dimensions of workspace
   color             m_good_color, m_bad_color;       // Colors of profitable and loss series
   color             m_back_color, m_fore_color;      // Background and frame colors
   bool              m_passes[];                      // Number of processed passes
   int               m_last_index;                    // Last pass index
public:
//--- Constructor/destructor
                     CColorProgressBar(void);
                    ~CColorProgressBar(void){};
                    
//--- Sets pointer to canvas
   void              SetCanvas(CCanvas *background, CCanvas *foreground)
                       {
                        if(background==NULL)
                          {
                           ::Print(__FUNCTION__, ": Error. Background is NULL");
                           return;
                          }
                        if(foreground==NULL)
                          {
                           ::Print(__FUNCTION__, ": Error. Foreground is NULL");
                           return;
                          }
                        
                        this.m_background=background;
                        this.m_foreground=foreground;
                       }
//--- Sets coordinates and dimensions of workspace on canvas
   void              SetBound(const int x1, const int y1, const int x2, const int y2)
                       {
                        this.m_bound.SetBound(x1, y1, x2, y2);
                       }
//--- Return of coordinates of bounds of rectangular area
   int               X1(void)                      const { return this.m_bound.left;   }
   int               Y1(void)                      const { return this.m_bound.top;    }
   int               X2(void)                      const { return this.m_bound.right;  }
   int               Y2(void)                      const { return this.m_bound.bottom; }
   
//--- Setting background color and frame
   void              SetBackColor(const color clr)       { this.m_back_color=clr;      }
   void              SetForeColor(const color clr)       { this.m_fore_color=clr;      }
//--- Returning background color and frame
   color             BackColor(void)               const { return this.m_back_color;   }
   color             ForeColor(void)               const { return this.m_fore_color;   }
//--- Resets counter to zero
   void              Reset(void)                         { this.m_last_index=0;        }
//--- Adds result for drawing strip in progress bar
   void              AddResult(bool good, const bool chart_redraw);
//--- Updates progress bar on chart
   void              Update(const bool chart_redraw);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CColorProgressBar::CColorProgressBar() : m_last_index(0), m_good_color(clrSeaGreen), m_bad_color(clrLightPink)
  {
//--- Set size of pass array with reserve
   ::ArrayResize(this.m_passes, 5000, 1000);
   ::ArrayInitialize(this.m_passes, 0);
  }
//+------------------------------------------------------------------+
//| Adding result                                            |
//+------------------------------------------------------------------+
void CColorProgressBar::AddResult(bool good, const bool chart_redraw)
  {
   this.m_passes[this.m_last_index]=good;
//--- Add another vertical line of desired color to progress bar
   this.m_foreground.LineVertical(this.X1()+1+this.m_last_index, this.Y1()+1, this.Y2()-1, ::ColorToARGB(good ? this.m_good_color : this.m_bad_color));
//--- Update on chart
   this.m_foreground.Update(chart_redraw);
//--- Updating index
   this.m_last_index++;
   if(this.m_last_index>=this.m_bound.Width()-1)
      this.m_last_index=0;
  }
//+------------------------------------------------------------------+
//| Updating progress bar on chart                                   |
//+------------------------------------------------------------------+
void CColorProgressBar::Update(const bool chart_redraw)
  {
//--- Fill background with background color
   this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color));
//--- Draw border
   this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color));
//--- Update chart
   this.m_background.Update(chart_redraw);
  }

The class doesn't have own canvas objects for drawing. To specify the canvas object on which to draw, there is a method to which a pointer to an existing canvas is passed. And it is assigned to class variables. Methods of the class will draw on this canvas. There are two objects here - one for drawing the background of the progress bar, and one for drawing in the foreground over the drawn background. CCanvas objects of a special chart class will act as canvas, on which this progress bar will be drawn.

A class for rendering statistics charts and tables of optimization results, as well as EA’s settings parameters:

//+------------------------------------------------------------------+
//| Class for rendering statistics charts and tables                 |
//| of optimization results and EA’s settings parameters         |
//+------------------------------------------------------------------+
class CStatChart: public CObject
  {
private:
   color             m_back_color;        // Background color
   color             m_fore_color;        // Border color
   int               m_line_width;        // Line width in pixels
   int               m_lines;             // Number of lines on chart
   CArrayDouble      m_seria[];           // Arrays for storing chart values
   bool              m_profitseria[];     // Profitable series or not
   int               m_lastseria_index;   // Index of fresh line on chart
   color             m_profit_color;      // Color of profitable series
   color             m_loss_color;        // Color of loss series
   color             m_selected_color;    // Color of selected best series
   
protected:
   CCanvas          *m_background;        // Pointer to object of CCanvas class for drawing on background
   CCanvas          *m_foreground;        // Pointer to CCanvas class object for drawing on foreground
   CRect             m_bound_chart;       // Workspace of chart
   CRect             m_bound_head;        // Chart header workspace
   CColorProgressBar m_progress_bar;      // Progress bar
   CButton           m_button_replay;     // Replay button
   CButtonSwitch     m_button_res;        // Button for selecting one of top three results
   int               m_tab_id;            // Tab ID
   
public:
//--- Constructor/destructor
                     CStatChart() : m_lastseria_index(0), m_profit_color(clrForestGreen), m_loss_color(clrOrangeRed), m_selected_color(clrDodgerBlue), m_tab_id(0) {};
                    ~CStatChart() { this.m_background=NULL; this.m_foreground=NULL; }
                    
//--- Sets pointer to canvas
   void              SetCanvas(CCanvas *background, CCanvas *foreground)
                       {
                        if(background==NULL)
                          {
                           ::Print(__FUNCTION__, ": Error. Background is NULL");
                           return;
                          }
                        if(foreground==NULL)
                          {
                           ::Print(__FUNCTION__, ": Error. Foreground is NULL");
                           return;
                          }
                        this.m_background=background;
                        this.m_foreground=foreground;
                        this.m_progress_bar.SetCanvas(background, foreground);
                       }
//--- Sets coordinates and dimensions of chart workspace and progress bar on canvas
   void              SetChartBounds(const int x1, const int y1, const int x2, const int y2)
                       {
                        this.m_bound_chart.SetBound(x1, y1, x2, y2);
                        this.SetBoundHeader(x1, y1-CELL_H, x2, y1);
                        this.m_progress_bar.SetBound(x1, y2-CELL_H, x2, y2);
                       }
//--- Sets coordinates and dimensions of chart header on canvas
   void              SetBoundHeader(const int x1, const int y1, const int x2, const int y2)
                       {
                        this.m_bound_head.SetBound(x1, y1, x2, y2);
                       }
//--- Returns pointer to (1) itself, (2) progress bar
   CStatChart       *Get(void)                           { return &this;                     }
   CColorProgressBar*GetProgressBar(void)                { return(&this.m_progress_bar);     }

//--- Setting/returning tab ID
   void              SetTabID(const int id)              { this.m_tab_id=id;                 }
   int               TabID(void)                   const { return this.m_tab_id;             }
   
//--- Returning coordinates of bounds of chart’s rectangular area
   int               X1(void)                      const { return this.m_bound_chart.left;   }
   int               Y1(void)                      const { return this.m_bound_chart.top;    }
   int               X2(void)                      const { return this.m_bound_chart.right;  }
   int               Y2(void)                      const { return this.m_bound_chart.bottom; }
//--- Return of coordinates of bounds of rectangular header area
   int               HeaderX1(void)                const { return this.m_bound_head.left;    }
   int               HeaderY1(void)                const { return this.m_bound_head.top;     }
   int               HeaderX2(void)                const { return this.m_bound_head.right;   }
   int               HeaderY2(void)                const { return this.m_bound_head.bottom;  }
//--- Return of coordinates of bounds of rectangular area of progress bar
   int               ProgressBarX1(void)           const { return this.m_progress_bar.X1();  }
   int               ProgressBarY1(void)           const { return this.m_progress_bar.Y1();  }
   int               ProgressBarX2(void)           const { return this.m_progress_bar.X2();  }
   int               ProgressBarY2(void)           const { return this.m_progress_bar.Y2();  }
   
//--- Returns pointer to button of: (1) replay, (2) result selection (3) worst, (4) average, (5) best result
   CButton          *ButtonReplay(void)                  { return(&this.m_button_replay);    }
   CButtonSwitch    *ButtonResult(void)                  { return(&this.m_button_res);       }
   CButtonTriggered *ButtonResultMin(void)               { return(this.m_button_res.GetButton(0)); }
   CButtonTriggered *ButtonResultMid(void)               { return(this.m_button_res.GetButton(1)); }
   CButtonTriggered *ButtonResultMax(void)               { return(this.m_button_res.GetButton(2)); }
   
//--- (1) Hides, (2) shows, (3) brings results selection button to foreground
   bool              ButtonsResultHide(void)             { return(this.m_button_res.Hide());       }
   bool              ButtonsResultShow(void)             { return(this.m_button_res.Show());       }
   bool              ButtonsResultBringToTop(void)       { return(this.m_button_res.BringToTop()); }
   
//--- Creates replay button
   bool              CreateButtonReplay(void)
                       {
                        if(this.m_background==NULL)
                          {
                           ::PrintFormat("%s: Фон не задан (сначала используйте функцию SetCanvas())");
                           return false;
                          }
                        string text="Optimization Completed: Click to Replay";
                        int w=this.m_background.TextWidth(text);
                        
                        //--- Upper-left coordinate of button
                        CPoint cp=this.m_bound_head.CenterPoint();
                        int x=cp.x-w/2;
                        int y=this.Y1()+this.m_bound_head.top-2;
                        
                        //--- Create button and set new colors for it, hide created button
                        if(!this.m_button_replay.Create(::StringFormat("Tab%d_ButtonReplay", this.m_tab_id), text, x, y, w, CELL_H-1))
                           return false;
                        
                        this.m_button_replay.SetDefaultColors(COLOR_BACKGROUND, STATE_OFF, C'144,238,144', C'144,228,144', C'144,218,144', clrSilver);
                        this.m_button_replay.SetDefaultColors(COLOR_BORDER, STATE_OFF, C'144,238,144', C'144,228,144', C'144,218,144', clrSilver);
                        this.m_button_replay.SetDefaultColors(COLOR_FOREGROUND, STATE_OFF, clrBlack, clrBlack, clrBlack, clrGray);
                        this.m_button_replay.ResetUsedColors(STATE_OFF);
                        this.m_button_replay.Draw(false);
                        this.m_button_replay.Hide();
                        return true;
                       }
                       
//--- Creates results selection button
   bool              CreateButtonResults(void)
                       {
                        if(this.m_background==NULL)
                          {
                           ::PrintFormat("%s: Фон не задан (сначала используйте функцию SetCanvas())");
                           return false;
                          }
                        //--- Upper-left coordinate of button
                        int x=this.m_bound_head.left+1;
                        int y=this.m_progress_bar.Y1()+CELL_H+2;
                        int w=BUTT_RES_W;
                        
                        //--- Creatу button and set new colors for it, hidу created button
                        if(!this.m_button_res.Create(::StringFormat("Tab%u_ButtonRes",this.m_tab_id), "", x, y, w, CELL_H-1))
                           return false;
                        
                        string text[3]={"Worst result of the top 3", "Average result of the top 3", "Best result of the top 3"};
                        if(!this.m_button_res.AddNewButton(text, w))
                           return false;
                        this.m_button_res.GetButton(0).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver);
                        this.m_button_res.GetButton(0).ResetUsedColors(STATE_OFF);
                        this.m_button_res.GetButton(1).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver);
                        this.m_button_res.GetButton(1).ResetUsedColors(STATE_OFF);
                        this.m_button_res.GetButton(2).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver);
                        this.m_button_res.GetButton(2).ResetUsedColors(STATE_OFF);
                        this.m_button_res.Draw(false);
                        this.m_button_res.Hide();
                        return true;
                       }

//--- Sets background color
   void              SetBackColor(const color clr)
                       {
                        this.m_back_color=clr;
                        this.m_progress_bar.SetBackColor(clr);
                       }
//--- Sets border color
   void              SetForeColor(const color clr)
                       {
                        this.m_fore_color=clr;
                        this.m_progress_bar.SetForeColor(clr);
                       }
   
//--- Sets number of lines on chart
   void              SetLines(const int num)
                       {
                        this.m_lines=num;
                        ::ArrayResize(this.m_seria, num);
                        ::ArrayResize(this.m_profitseria, num);
                       }
                       
//--- Setting color of (1) profitable, (2) loss, (3) selected series
   void              SetProfitColorLine(const color clr)    { this.m_profit_color=clr;    }
   void              SetLossColorLine(const color clr)      { this.m_loss_color=clr;      }
   void              SetSelectedLineColor(const color clr)  { this.m_selected_color=clr;  }
                       
//--- Updating object on screen
   void              Update(color clr, const int line_width, const bool chart_redraw);
//--- Adding data from array
   void              AddSeria(const double  &array[], bool profit);
//--- Draws chart 
   void              Draw(const int seria_index, color clr, const int line_width, const bool chart_redraw);
//--- Draws line in usual coordinates (from left to right, from bottom to top)
   void              Line(int x1, int y1, int x2, int y2, uint col, int size);
//--- Getting max. and min. values in the series 
   double            MaxValue(const int seria_index);
   double            MinValue(const int seria_index);
   
//--- Event handler
   void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
     {
      //--- If replay button is not hidden, call its event handler
      if(!this.m_button_replay.IsHidden())
         this.m_button_replay.OnChartEvent(id, lparam, dparam, sparam);
      //--- If result selection button is not hidden, call its event handler
      if(!this.m_button_res.IsHidden())
         this.m_button_res.OnChartEvent(id, lparam, dparam, sparam);
     }
  };

On the specified canvas (background and foreground) the class draws tables of parameters and test results, charts of passes, a progress bar, and buttons to start replay of the completed optimization process and select the best results on certain optimization criteria.

It should be noted that to specify bounds of the rectangular area of the canvas inside which the tracked object or area is located the classes discussed here use the CRect structure.

The structure is described in \MQL5\Include\Controls\Rect.mqh, and it serves as a convenient tool for specifying bounds of a rectangular area containing important elements. For example, on the canvas we can limit the area within which the mouse cursor should be tracked, or we can specify the size of the bounding rectangle for the entire canvas size. In this case, the entire area of the entire object will be available for interaction with the cursor. The methods that return coordinates of the bounds of a rectangular area are created within the structure. Moreover, it is possible to set bounds and obtain their values in several ways — it all depends on the needs and structure of objects. Methods for moving and shifting a rectangular area are also implemented. In general, it is a convenient tool for specifying bounds of any area that must be tracked in any way.

In the classes under consideration, these areas are required to interact with the mouse cursor and indicate where objects are located on the canvas.

A method for updating the chart:

//+------------------------------------------------------------------+
//| Updating chart                                                   |
//+------------------------------------------------------------------+
void CStatChart::Update(color clr, const int line_width, const bool chart_redraw)
  {
//--- If canvas for background or foreground is not installed, exit
   if(this.m_background==NULL || this.m_foreground==NULL)
      return;
//--- StatChart fill in background
   this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color));
//--- StatChart draw border
   this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color));
   
//--- ProgressBar fill in background and draw border
   this.m_progress_bar.Update(false);
   
//--- Draw each series for 80% of available chart area vertically and horizontally
   for(int i=0; i<this.m_lines; i++)
     {
      //--- If color is set missing, use colors of profitable and loss series
      if(clr==clrNONE)
        {
         clr=this.m_loss_color;
         if(this.m_profitseria[i])
            clr=this.m_profit_color;
        }
      //--- otherwise, use color set for selected line
      else
         clr=this.m_selected_color;
      
      //--- Draw a chart of optimization results
      this.Draw(i, clr, line_width, false);
     }
//--- Update both canvases
   this.m_background.Update(false);
   this.m_foreground.Update(chart_redraw);
  }

The rectangular area of the canvas intended for drawing passage charts is erased, and a balance line and a progress bar are drawn on it.

A method that adds a new series of data to be drawn on a chart:

//+------------------------------------------------------------------+
//| Adds new series of data to be drawn on chart                     |
//+------------------------------------------------------------------+
void CStatChart::AddSeria(const double &array[], bool profit)
  {
//--- Adding array to series number m_lastseria_index
   this.m_seria[this.m_lastseria_index].Resize(0);
   this.m_seria[this.m_lastseria_index].AddArray(array);
   this.m_profitseria[this.m_lastseria_index]=profit;
//--- Track index of last line (not currently in use)
   this.m_lastseria_index++;
   if(this.m_lastseria_index>=this.m_lines)
      this.m_lastseria_index=0;
  }

Each new pass of the optimizer, its data array, must be entered into the series array, which is what this method implements.

Methods for getting the maximum and minimum values of the specified series in the array of optimizer passes:

//+------------------------------------------------------------------+
//| Getting maximum value of specified series                        |
//+------------------------------------------------------------------+
double CStatChart::MaxValue(const int seria_index)
  {
   double res=this.m_seria[seria_index].At(0);
   int total=this.m_seria[seria_index].Total();
//--- Iterate through array and compare every two adjacent series
   for(int i=1; i<total; i++)
     {
      if(this.m_seria[seria_index].At(i)>res)
         res=this.m_seria[seria_index].At(i);
     }
//--- result
   return res;
  }
//+------------------------------------------------------------------+
//| Getting minimum value of specified series                  |
//+------------------------------------------------------------------+
double CStatChart::MinValue(const int seria_index)
  {
   double res=this.m_seria[seria_index].At(0);;
   int total=this.m_seria[seria_index].Total();
//--- Iterate through array and compare every two adjacent series
   for(int i=1; i<total; i++)
     {
      if(this.m_seria[seria_index].At(i)<res)
         res=this.m_seria[seria_index].At(i);
     }
//--- result
   return res;
  }

To position the optimizer's pass charts relative to the center of a special chart, you should know the maximum and minimum values in the pass series. Then, using these values, you can calculate the relative coordinates of the line on the chart so that the line fits into 80% of the chart space allocated for drawing balance charts of optimizer passes.

A method for drawing a balance line on a chart:

//+------------------------------------------------------------------+
//| Overloading the basic drawing function                           |
//+------------------------------------------------------------------+
void CStatChart::Line(int x1, int y1, int x2, int y2, uint col, int size)
  {
//--- If canvas is not set, exit
   if(this.m_foreground==NULL)
      return;
//--- Since Y-axis is inverted, invert y1 and y2
   int y1_adj=this.m_bound_chart.Height()-CELL_H-y1;
   int y2_adj=this.m_bound_chart.Height()-CELL_H-y2;
   
//--- Draw smoothed line
//--- If line thickness is less than 3, draw line using the Wu smoothing algorithm
//--- (for thicknesses of 1 and 2, LineWu() method is called in LineThick() method),
//--- otherwise, draw smoothed line of given thickness using LineThick
   this.m_foreground.LineThick(x1, y1_adj, x2, y2_adj,::ColorToARGB(col), (size<1 ? 1 : size), STYLE_SOLID, LINE_END_ROUND);
  }

This is an overloaded method of CCanvas class method of the same name. Coordinates on the chart start from the upper-left corner. And usual coordinates of balance charts start from the bottom left one.

In this method, the screen Y coordinates are flipped over to draw a non-inverted balance line based on values of balance points from the array.

A method that draws balance lines on a chart:

//+------------------------------------------------------------------+
//| Drawing balance line on chart                                    |
//+------------------------------------------------------------------+
void CStatChart::Draw(const int seria_index, color clr, const int line_width, const bool chart_redraw)
  {
//--- If canvas is not set, exit
   if(this.m_foreground==NULL)
      return;
   
//--- Preparing coefficients for converting values into pixels
   double min=this.MaxValue(seria_index);
   double max=this.MinValue(seria_index);
   double size=this.m_seria[seria_index].Total();
   
//--- Indentations from chart edge
   double x_indent=this.m_bound_chart.Width()*0.05;
   double y_indent=this.m_bound_chart.Height()*0.05;
   
//--- Calculate coefficients
   double k_y=(max-min)/(this.m_bound_chart.Height()-2*CELL_H-2*y_indent);
   double k_x=(size)/(this.m_bound_chart.Width()-2*x_indent);
   
//--- Coefficients
   double start_x=this.m_bound_chart.left+x_indent;
   double start_y=this.m_bound_chart.bottom-2*CELL_H*2-y_indent;
   
//--- Now draw polyline passing through all points of series
   for(int i=1; i<size; i++)
     {
      //--- convert values to pixels
      int x1=(int)((i-0)/k_x+start_x);  // set value number horizontally
      int y1=(int)(start_y-(m_seria[seria_index].At(i)-min)/k_y);    // vertically
      int x2=(int)((i-1-0)/k_x+start_x);// set value number horizontally
      int y2=(int)(start_y-(m_seria[seria_index].At(i-1)-min)/k_y);  // vertically
      //--- Draw line from previous point to current one
      this.Line(x1, y1, x2, y2, clr, line_width);
     }
//--- Updating canvas with chart redrawing (if flag is set)
   this.m_foreground.Update(chart_redraw);
  }

Here, the necessary coordinates of the balance line on the chart are calculated (inside the chart area intended for drawing balance charts), and in a loop, according to the array of the specified series, draw lines between all the balance points recorded in the array.

Frame data class:

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_FRAME_PROP                                              // Frame properties
  {
   FRAME_PROP_PASS_NUM,                                           // Pass number
   FRAME_PROP_SHARPE_RATIO,                                       // Sharpe Ratio result
   FRAME_PROP_NET_PROFIT,                                         // Net Profit result
   FRAME_PROP_PROFIT_FACTOR,                                      // Profit Factor result
   FRAME_PROP_RECOVERY_FACTOR,                                    // Recovery Factor result
  };
//+------------------------------------------------------------------+
//| Frame data class                                                 |
//+------------------------------------------------------------------+
class CFrameData : public CObject
  {
protected:
   ulong             m_pass;                                      // Pass number
   double            m_sharpe_ratio;                              // Sharpe Ratio
   double            m_net_profit;                                // Total profit
   double            m_profit_factor;                             // Profitability
   double            m_recovery_factor;                           // Recovery factor
public:
//--- Setting frame properties (pass results)
   void              SetPass(const ulong pass)                    { this.m_pass=pass;              }
   void              SetSharpeRatio(const double value)           { this.m_sharpe_ratio=value;     }
   void              SetNetProfit(const double value)             { this.m_net_profit=value;       }
   void              SetProfitFactor(const double value)          { this.m_profit_factor=value;    }
   void              SetRecoveryFactor(const double value)        { this.m_recovery_factor=value;  }
//--- Returning frame properties (pass results)
   ulong             Pass(void)                             const { return this.m_pass;            }
   double            SharpeRatio(void)                      const { return this.m_sharpe_ratio;    }
   double            NetProfit(void)                        const { return this.m_net_profit;      }
   double            ProfitFactor(void)                     const { return this.m_profit_factor;   }
   double            RecoveryFactor(void)                   const { return this.m_recovery_factor; }

//--- Description of properties
   string            PassDescription(void)                  const { return ::StringFormat("Pass: %I64u", this.m_pass);                       }
   string            SharpeRatioDescription(void)           const { return ::StringFormat("Sharpe Ratio: %.2f", this.m_sharpe_ratio);        }
   string            NetProfitDescription(void)             const { return ::StringFormat("Net Profit: %.2f", this.m_net_profit);            }
   string            ProfitFactorDescription(void)          const { return ::StringFormat("Profit Factor: %.2f", this.m_profit_factor);      }
   string            RecoveryFactorDescription(void)        const { return ::StringFormat("Recovery Factor: %.2f", this.m_recovery_factor);  }

//--- Printing frame properties to log
   void              Print(void)
                       {
                        ::PrintFormat("Frame %s:", this.PassDescription());
                        ::PrintFormat(" - %s", this.SharpeRatioDescription());
                        ::PrintFormat(" - %s", this.NetProfitDescription());
                        ::PrintFormat(" - %s", this.ProfitFactorDescription());
                        ::PrintFormat(" - %s", this.RecoveryFactorDescription());
                       }

//--- A method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        //--- Compare real values as two-digit values
                        const CFrameData *obj=node;
                        switch(mode)
                          {
                           case FRAME_PROP_SHARPE_RATIO     :  return(::NormalizeDouble(this.SharpeRatio(),2)   > ::NormalizeDouble(obj.SharpeRatio(),2)    ?  1 :
                                                                      ::NormalizeDouble(this.SharpeRatio(),2)   < ::NormalizeDouble(obj.SharpeRatio(),2)    ? -1 : 0);
                           case FRAME_PROP_NET_PROFIT       :  return(::NormalizeDouble(this.NetProfit(),2)     > ::NormalizeDouble(obj.NetProfit(),2)      ?  1 :
                                                                      ::NormalizeDouble(this.NetProfit(),2)     < ::NormalizeDouble(obj.NetProfit(),2)      ? -1 : 0);
                           case FRAME_PROP_PROFIT_FACTOR    :  return(::NormalizeDouble(this.ProfitFactor(),2)  > ::NormalizeDouble(obj.ProfitFactor(),2)   ?  1 :
                                                                      ::NormalizeDouble(this.ProfitFactor(),2)  < ::NormalizeDouble(obj.ProfitFactor(),2)   ? -1 : 0);
                           case FRAME_PROP_RECOVERY_FACTOR  :  return(::NormalizeDouble(this.RecoveryFactor(),2)> ::NormalizeDouble(obj.RecoveryFactor(),2) ?  1 :
                                                                      ::NormalizeDouble(this.RecoveryFactor(),2)< ::NormalizeDouble(obj.RecoveryFactor(),2) ? -1 : 0);
                           //---FRAME_PROP_PASS_NUM
                           default                          :  return(this.Pass()>obj.Pass() ?  1  :  this.Pass()<obj.Pass()  ? -1  :  0);
                          }
                       }
   
//--- Constructors/destructor
                     CFrameData (const ulong pass, const double sharpe_ratio, const double net_profit, const double profit_factor, const double recovery_factor) :
                        m_pass(pass), m_sharpe_ratio(sharpe_ratio), m_net_profit(net_profit), m_profit_factor(profit_factor), m_recovery_factor(recovery_factor) {}
                     CFrameData (void) :
                        m_pass(0), m_sharpe_ratio(0), m_net_profit(0), m_profit_factor(0), m_recovery_factor(0) {}
                    ~CFrameData (void) {}
  };

After each pass of the optimizer is completed, a frame is sent to the terminal. It contains all the data that was received at the end of this pass. To access the data of any pass, one should search for a frame with the desired number in a loop through all the received frames and retrieve its data. This is not expedient at all. We should be able to quickly access the data of the desired pass, and be able to sort all the passes by the specified property, since we will need to select the top three passes: one from each of the four optimization criteria.

The way out is to cache passes. To do this, we need a frame object class. After completing each pass and sending a frame to the terminal, you need to create a frame object, fill in its properties with the data of the received test frame and locate the frame object in the list. Further, after the optimization process is completed and all frames are retrieved, we will have copies of all frames in the frame list. And now you can sort this list of frames by required properties and quickly retrieve data of the desired frame from it.

It is worth noting that in Compare() method, we had to compare real numbers not by comparing the normalized difference with zero, but by comparing two normalized numbers with each other. Why so?

There are different ways to compare two real numbers. The first one is to compare unnormalized numbers. First, compare with the ternary operator by "more", then by "less", and at the end, what is left means "equal". Alternatively, you can compare the normalized difference of two numbers with zero. However, here we had to normalize both numbers to two digits, and compare these values.

The fact is that in the terminal, the results table shows two-digit numbers in optimization results. But internally, these numbers are not normalized to two digits. In other words, a two-digit representation of the results is reflected only in the results table. And if there are values in the table, for example, 1.09 and 1.08, then in fact this may not be the case. There may be such numbers as: 1.085686399864 and 1.081254322375. Both numbers are rounded up to 1.09 and 1.08 in the table. But when comparing, you may come across the fact that both numbers are rounded by normalization to the same value. And if not normalize, then the value of 1.09 may be missing. And this will lead to an incorrect search for the best passes.

The solution is to normalize both numbers to two digits, and only then compare their rounded values.

Frame Viewer Class:

//+------------------------------------------------------------------+
//| ▸Frame viewer class                                     |
//+------------------------------------------------------------------+
class CFrameViewer : public CObject
  {
private:
   int               m_w;                       // Chart width
   int               m_h;                       // Chart height
   color             m_selected_color;          // Color of selected series from top three
   uint              m_line_width;              // Width of line of selected series from top three
   bool              m_completed;               // Optimization completion flag
   
   CFrameData        m_frame_tmp;               // Frame object for searching by property
   CArrayObj         m_list_frames;             // List of frames
   CTabControl       m_tab_control;             // Tab Control
   
//--- Declare tab objects on Tab Control
//--- Tab 0 (Optimization) of Tab Control
   CTableDataControl m_table_inp_0;             // Table of optimization parameters on tab 0
   CTableDataControl m_table_stat_0;            // Table of optimization results on tab 0
   CTableDataControl m_table_stat_0;            // Table of optimization results on tab 0
   CColorProgressBar*m_progress_bar;            // Progress bar on optimization chart on tab 0
   
//--- Tab 1 (Sharpe Ratio) of Tab Control
   CTableDataControl m_table_inp_1;             // Table of optimization parameters on tab 1
   CTableDataControl m_table_stat_1;            // Table of optimization results on tab 1
   CStatChart        m_chart_stat_1;            // Table of optimization results on tab 1
   
//--- Tab 2 (Net Profit) of Tab Control
   CTableDataControl m_table_inp_2;             // Table of optimization parameters on tab 2
   CTableDataControl m_table_stat_2;            // Table of optimization results on tab 2
   CStatChart        m_chart_stat_2;            // Chart of optimization results on tab 2
   
//--- Tab 3 (Profit Factor) of Tab Control
   CTableDataControl m_table_inp_3;             // Table of optimization parameters on tab 3
   CTableDataControl m_table_stat_3;            //  Table of optimization results on tab 3
   CStatChart        m_chart_stat_3;            // Chart of optimization results on tab 3
   
//--- Tab 4 (Recovery Factor) of Tab Control
   CTableDataControl m_table_inp_4;             // Table of optimization parameters on tab 4
   CTableDataControl m_table_stat_4;            // able of optimization results on tab 4
   CStatChart        m_chart_stat_4;            // Chart of optimization results on tab 4

protected:
//--- Returns pointer to table of optimization parameters by tab index
   CTableDataControl*GetTableInputs(const uint tab_id)
                       {
                        switch(tab_id)
                          {
                           case 0 : return this.m_table_inp_0.Get();
                           case 1 : return this.m_table_inp_1.Get();
                           case 2 : return this.m_table_inp_2.Get();
                           case 3 : return this.m_table_inp_3.Get();
                           case 4 : return this.m_table_inp_4.Get();
                           default: return NULL;
                          }
                       }

//--- Returns pointer to table of optimization results by tab index
   CTableDataControl*GetTableStats(const uint tab_id)
                       {
                        switch(tab_id)
                          {
                           case 0 : return this.m_table_stat_0.Get();
                           case 1 : return this.m_table_stat_1.Get();
                           case 2 : return this.m_table_stat_2.Get();
                           case 3 : return this.m_table_stat_3.Get();
                           case 4 : return this.m_table_stat_4.Get();
                           default: return NULL;
                          }
                       }

//--- Returns pointer to chart of optimization results by tab index
   CStatChart       *GetChartStats(const uint tab_id)
                       {
                        switch(tab_id)
                          {
                           case 0 : return this.m_chart_stat_0.Get();
                           case 1 : return this.m_chart_stat_1.Get();
                           case 2 : return this.m_chart_stat_2.Get();
                           case 3 : return this.m_chart_stat_3.Get();
                           case 4 : return this.m_chart_stat_4.Get();
                           default: return NULL;
                          }
                       }

//--- Adds frame object to list
   bool              AddFrame(CFrameData *frame)
                       {
                        if(frame==NULL)
                          {
                           ::PrintFormat("%s: Error: Empty object passed",__FUNCTION__);
                           return false;
                          }
                        this.m_frame_tmp.SetPass(frame.Pass());
                        this.m_list_frames.Sort(FRAME_PROP_PASS_NUM);
                        int index=this.m_list_frames.Search(frame);
                        if(index>WRONG_VALUE)
                           return false;
                        return this.m_list_frames.Add(frame);
                       }

//--- Draws table of optimization statistics on specified tab
   void              TableStatDraw(const uint tab_id, const int x, const int y, const int w, const int h, const bool chart_redraw);
//--- Draws table of input optimization parameters on specified tab
   void              TableInpDraw(const uint tab_id, const int x, const int y, const int w, const int h, const uint rows, const bool chart_redraw);
//--- Draws chart of optimization on specified tab
   void              ChartOptDraw(const uint tab_id, const bool opt_completed, const bool chart_redraw);
//--- Draws data tables and optimization chart
   void              DrawDataChart(const uint tab_id);
//--- Draws charts of top three passes by optimization criterion
   void              DrawBestFrameData(const uint tab_id, const int res_index);
//--- Controls view of control objects on optimization charts
   void              ControlObjectsView(const uint tab_id);

//--- Replaying frames after optimization completion
   void              ReplayFrames(const int delay_ms);
//--- Retrieving data of current frame and printing it on specified tab in table and on optimization results chart
   bool              DrawFrameData(const uint tab_id, const string text, color clr, const uint line_width, ulong &pass, string &params[], uint &par_count, double &data[]);
//--- Prints data of specified frame to optimization chart
   bool              DrawFrameDataByPass(const uint tab_id, const ulong pass_num, const string text, color clr, const uint line_width, double &data[]);
//--- Fills array with frame indexes of top three passes for specified optimization criterion (by tab index)
   bool              FillArrayBestFrames(const uint tab_id, ulong &array_passes[]);
//--- Prints out three best passes on each tab on optimization results charts
   void              DrawBestFrameDataAll(void);
//--- Searches for and returns pointer to frame object with a property value less than sample
   CFrameData       *FrameSearchLess(CFrameData *frame, const int mode);
   
public:
//--- Setting thickness of selected line
   void              SetSelectedLineWidth(const uint width)    { this.m_line_width=width; }

//--- Setting color of profitable series
   void              SetProfitColorLine(const color clr)
                       {
                        int total=this.m_tab_control.TabsTotal();
                        for(int i=1; i<total; i++)
                          {
                           CStatChart *chart=this.GetChartStats(i);
                           if(chart!=NULL)
                              chart.SetProfitColorLine(clr);
                          }
                       }
//--- Setting color of loss series
   void              SetLossColorLine(const color clr)
                       {
                        int total=this.m_tab_control.TabsTotal();
                        for(int i=1; i<total; i++)
                          {
                           CStatChart *chart=this.GetChartStats(i);
                           if(chart!=NULL)
                              chart.SetLossColorLine(clr);
                          }
                       }
//--- Setting color of selected series
   void              SetSelectedLineColor(const color clr)
                       {
                        int total=this.m_tab_control.TabsTotal();
                        for(int i=1; i<total; i++)
                          {
                           CStatChart *chart=this.GetChartStats(i);
                           if(chart!=NULL)
                              chart.SetSelectedLineColor(clr);
                          }
                       }
   
//--- Event handlers of strategy tester
   void              OnTester(const double OnTesterValue);
   int               OnTesterInit(const int lines, const int selected_line_width, const color selected_line_color);
   void              OnTesterPass(void);
   void              OnTesterDeinit(void);
   
//--- Chart event handlers
   void              OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam,const int delay_ms);
   
protected:
//--- Handler for (1) changing tab of Tab Control element, (2) selecting Button Switch
   void              OnTabSwitchEvent(const int tab_id);
   void              OnButtonSwitchEvent(const int tab_id, const uint butt_id);
   
public:   
//--- Constructor/destructor
                     CFrameViewer(void);
                    ~CFrameViewer(void){ this.m_list_frames.Clear(); }
  };

We know exactly how many tabs there will be, and which elements will be located on each tab. Therefore, there is no creation of new objects here, but simply instances of the necessary objects for each tab, access methods to them, and methods for the class to work are declared.

Constructors

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameViewer::CFrameViewer(void) : m_completed(false), m_progress_bar(NULL), m_selected_color(clrDodgerBlue), m_line_width(1)
  {
//--- Chart window size
   this.m_w=(int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   this.m_h=(int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
//--- Get pointer to progress bar from statistics chart object
   this.m_progress_bar=this.m_chart_stat_0.GetProgressBar();
   this.m_list_frames.Clear();
  }

In the constructor, we get and store the width and height of the chart on which the Expert Advisor is running, find and write a pointer to the progress bar and clear the list of frames.

When launching optimization, before it starts, prepare a chart on which a copy of the Expert Advisor in frame mode will be launched in the client terminal. The chart will be detached from the terminal, Tab Control will be located on it in its full size. And the remaining elements will be located on its tabs, which will display balance charts of passes and control buttons.

All this must be done in OnTesterInit() handler. To do this, the class provides handlers of the same name, which are launched in the Expert Advisor from an instance of CFrameViewer class.

Handler OnTesterInit:

//+------------------------------------------------------------------+
//| It must be called in handler of OnTesterInit() Expert Advisor          | 
//+------------------------------------------------------------------+
int CFrameViewer::OnTesterInit(const int lines, const int selected_line_width, const color selected_line_color)
  {
//--- Chart ID with expert running in Frame mode
   long chart_id=::ChartID();
   
//--- Preparing a floating chart for drawing statistics tables and balance lines
   ::ResetLastError();
   if(!::ChartSetInteger(chart_id, CHART_SHOW, false))
     {
      ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError());
      return INIT_FAILED;
     }
   if(!::ChartSetInteger(chart_id, CHART_IS_DOCKED, false))
     {
      ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError());
      return INIT_FAILED;
     }
//--- Clearing chart completely of all graphical objects
   ::ObjectsDeleteAll(chart_id);
   
//--- Based on chart size create Tab Control with five tabs
   int w=(int)::ChartGetInteger(chart_id, CHART_WIDTH_IN_PIXELS);
   int h=(int)::ChartGetInteger(chart_id, CHART_HEIGHT_IN_PIXELS);
   if(this.m_tab_control.Create("TabControl", "", 0, 0, w, h))
     {
      //--- If control is created successfully, add five tabs to it
      bool res=true;
      for(int i=0; i<5; i++)
        {
         string tab_text=(i==1 ? "Sharpe Ratio" : i==2 ? "Net Profit" : i==3 ? "Profit Factor" : i==4 ? "Recovery Factor" : "Optimization");
         res &=this.m_tab_control.AddTab(i, tab_text);
        }
      if(!res)
        {
         ::PrintFormat("%s: Errors occurred while adding tabs to the Tab Control",__FUNCTION__);
         return INIT_FAILED;
        }
     }
   else
     {
      Print("Tab Control creation failed");
      return INIT_FAILED;
     }
   
//--- CCanvas objects in workspace of tab 0 (Optimization) for drawing background images and text
   CCanvas *tab0_background=this.m_tab_control.GetTabBackground(0);
   CCanvas *tab0_foreground=this.m_tab_control.GetTabForeground(0);
//--- CCanvas objects in workspace of tab 1 (Sharpe Ratio) for drawing background images and text
   CCanvas *tab1_background=this.m_tab_control.GetTabBackground(1);
   CCanvas *tab1_foreground=this.m_tab_control.GetTabForeground(1);
//--- CCanvas objects in workspace of tab 2 (Net Profit) for drawing background images and text
   CCanvas *tab2_background=this.m_tab_control.GetTabBackground(2);
   CCanvas *tab2_foreground=this.m_tab_control.GetTabForeground(2);
//--- CCanvas objects in workspace of tab 3 (Profit Factor) for drawing background images and text
   CCanvas *tab3_background=this.m_tab_control.GetTabBackground(3);
   CCanvas *tab3_foreground=this.m_tab_control.GetTabForeground(3);
//--- CCanvas objects in workspace of tab 4 (Recovery Factor) for drawing background images and text
   CCanvas *tab4_background=this.m_tab_control.GetTabBackground(4);
   CCanvas *tab4_foreground=this.m_tab_control.GetTabForeground(4);
   
//--- Set tab identifiers for objects of optimization statistics charts
   this.m_chart_stat_0.SetTabID(0);
   this.m_chart_stat_1.SetTabID(1);
   this.m_chart_stat_2.SetTabID(2);
   this.m_chart_stat_3.SetTabID(3);
   this.m_chart_stat_4.SetTabID(4);
   
//--- Indicate for objects of statistics charts that we draw on tab with corresponding index
   this.m_chart_stat_0.SetCanvas(tab0_background, tab0_foreground);
   this.m_chart_stat_1.SetCanvas(tab1_background, tab1_foreground);
   this.m_chart_stat_2.SetCanvas(tab2_background, tab2_foreground);
   this.m_chart_stat_3.SetCanvas(tab3_background, tab3_foreground);
   this.m_chart_stat_4.SetCanvas(tab4_background, tab4_foreground);
   
//--- Set number of series on optimization statistics charts
   this.m_chart_stat_0.SetLines(lines);
   this.m_chart_stat_1.SetLines(lines);
   this.m_chart_stat_2.SetLines(lines);
   this.m_chart_stat_3.SetLines(lines);
   this.m_chart_stat_4.SetLines(lines);
   
//--- Setting background and foreground colors of optimization statistics charts
   this.m_chart_stat_0.SetBackColor(clrIvory);
   this.m_chart_stat_0.SetForeColor(C'200,200,200');
   this.m_chart_stat_1.SetBackColor(clrIvory);
   this.m_chart_stat_1.SetForeColor(C'200,200,200');
   this.m_chart_stat_2.SetBackColor(clrIvory);
   this.m_chart_stat_2.SetForeColor(C'200,200,200');
   this.m_chart_stat_3.SetBackColor(clrIvory);
   this.m_chart_stat_3.SetForeColor(C'200,200,200');
   this.m_chart_stat_4.SetBackColor(clrIvory);
   this.m_chart_stat_4.SetForeColor(C'200,200,200');
   
//--- Set thickness and color of selected line of best pass
   this.SetSelectedLineWidth(selected_line_width);
   this.SetSelectedLineColor(selected_line_color);
   
//--- Draw two tables with optimization results and input parameters on tab 0 (Optimization),
//--- and window with progress bar for printing charts and optimization process
   this.TableStatDraw(0, 4, 4, CELL_W*2, CELL_H, false);
   this.TableInpDraw(0, 4, this.m_table_stat_0.Y2()+4, CELL_W*2, CELL_H, 0, false);
   this.ChartOptDraw(0, this.m_completed, true);
//--- Create optimization replay button on tab 0
   if(!this.m_chart_stat_0.CreateButtonReplay())
     {
      Print("Button Replay creation failed");
      return INIT_FAILED;
     }
   
//--- Draw two tables with optimization results and input parameters on tab 1 (Sharpe Ratio),
//--- and window to print charts of optimization results
   this.TableStatDraw(1, 4, 4, CELL_W*2, CELL_H, false);
   this.TableInpDraw(1, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false);
   this.ChartOptDraw(1, this.m_completed, true);
//--- Create result selection button on tab 1
   if(!this.m_chart_stat_1.CreateButtonResults())
     {
      Print("Tab1: There were errors when creating the result buttons");
      return INIT_FAILED;
     }
     
//--- Draw two tables with optimization results and input parameters on tab 2 (Net Profit),
//--- and window to print charts of optimization results
   this.TableStatDraw(2, 4, 4, CELL_W*2, CELL_H, false);
   this.TableInpDraw(2, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false);
   this.ChartOptDraw(2, this.m_completed, true);
//--- Create result selection button on tab 2
   if(!this.m_chart_stat_2.CreateButtonResults())
     {
      Print("Tab2: There were errors when creating the result buttons");
      return INIT_FAILED;
     }
     
//--- Draw two tables with optimization results and input parameters on tab 3 (Profit Factor),
//--- and window to print charts of optimization results
   this.TableStatDraw(3, 4, 4, CELL_W*2, CELL_H, false);
   this.TableInpDraw(3, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false);
   this.ChartOptDraw(3, this.m_completed, true);
//--- Create result selection button on tab 3
   if(!this.m_chart_stat_3.CreateButtonResults())
     {
      Print("Tab3: There were errors when creating the result buttons");
      return INIT_FAILED;
     }
     
//--- Draw two tables with optimization results and input parameters on tab 4 (Recovery Factor),
//--- and window to print charts of optimization results
   this.TableStatDraw(4, 4, 4, CELL_W*2, CELL_H, false);
   this.TableInpDraw(4, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false);
   this.ChartOptDraw(4, this.m_completed, true);
//--- Create result selection button on tab 4
   if(!this.m_chart_stat_4.CreateButtonResults())
     {
      Print("Tab4: There were errors when creating the result buttons");
      return INIT_FAILED;
     }
     
   return INIT_SUCCEEDED;
  }

Here, all elements are created block-by-block. Each block of code is responsible for creating some element of the program interface.

After the optimization is completed, it is necessary to make some changes to the created interface — to repaint chart headers, change texts on them and print the replay start button on the first tab (with identifier of 0). All this must be implemented in OnTesterDeinit() handler.

Handler OnTesterDeinit:

//+------------------------------------------------------------------+
//| It must be called in handler of OnTesterDeinit() Expert Advisor  |
//+------------------------------------------------------------------+
void CFrameViewer::OnTesterDeinit(void)
  {
//--- Get pointers to canvas to draw background and foreground
   CCanvas *background=this.m_tab_control.GetTabBackground(0);
   CCanvas *foreground=this.m_tab_control.GetTabForeground(0);
   
   if(background==NULL || foreground==NULL)
      return;
   
//--- Set optimization completion flag
   this.m_completed=true;
   
//--- Chart header coordinates
   int x1=this.m_chart_stat_0.HeaderX1();
   int y1=this.m_chart_stat_0.HeaderY1();
   int x2=this.m_chart_stat_0.HeaderX2();
   int y2=this.m_chart_stat_0.HeaderY2();
      
   int x=(x1+x2)/2;
   int y=(y1+y2)/2;
      
//--- Repaint background and erase header text
   background.FillRectangle(x1, y1, x2, y2, ::ColorToARGB(clrLightGreen));
   foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
   
//--- Change text and color of header 
   string text="Optimization Complete: Click to Replay";
   foreground.FontSet("Calibri", -100, FW_BLACK);
   foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
   background.Update(false);
   foreground.Update(true);
   
//--- Get active tab index and call method for controlling print of control objects on optimization charts
   int tab_selected=this.m_tab_control.GetSelectedTabID();
   this.ControlObjectsView(tab_selected);
   
//--- On each tab (1-4), draw charts of three best optimization passes
   this.DrawBestFrameDataAll();
   ::ChartRedraw();
  }

Upon completion of each optimizer pass, a Tester event is generated, which can be handled in OnTester() handler. It is launched on the side of EA instance running on the testing agent.

In this handler, it is necessary to collect all the data about the completed pass, form a frame and send it to the client terminal using FrameAdd() function.

Handler OnTester:

//+------------------------------------------------------------------+
//| Prepares array of balance values and sends it in frame           |
//| It must be called in Expert Advisor in OnTester() handler        |
//+------------------------------------------------------------------+
void CFrameViewer::OnTester(const double OnTesterValue)
  {
//--- Variables for working with pass results
   double balance[];
   int    data_count=0;
   double balance_current=::TesterStatistics(STAT_INITIAL_DEPOSIT);

//--- Temporary variables for working with trades
   ulong  ticket=0;
   double profit;
   string symbol;
   long   entry;
   
//--- Request entire trading history
   ::ResetLastError();
   if(!::HistorySelect(0, ::TimeCurrent()))
     {
      PrintFormat("%s: HistorySelect() failed. Error ",__FUNCTION__, ::GetLastError());
      return;
     }
     
//--- Collect data of trades
   uint deals_total=::HistoryDealsTotal();
   for(uint i=0; i<deals_total; i++)
     {
      ticket=::HistoryDealGetTicket(i);
      if(ticket==0)
         continue;
      symbol=::HistoryDealGetString(ticket, DEAL_SYMBOL);
      entry =::HistoryDealGetInteger(ticket, DEAL_ENTRY);
      profit=::HistoryDealGetDouble(ticket, DEAL_PROFIT);
      if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_INOUT)
         continue;

      balance_current+=profit;
      data_count++;
      ::ArrayResize(balance, data_count);
      balance[data_count-1]=balance_current;
     }
//--- data[] array to send data to frame
   double data[];
   ::ArrayResize(data, ::ArraySize(balance)+DATA_COUNT);
   ::ArrayCopy(data, balance, DATA_COUNT, 0);
   
//--- Fill in first DATA_COUNT values of array with test results
   data[0]=::TesterStatistics(STAT_SHARPE_RATIO);           // Sharpe Ratio
   data[1]=::TesterStatistics(STAT_PROFIT);                 // net profit
   data[2]=::TesterStatistics(STAT_PROFIT_FACTOR);          // profit factor
   data[3]=::TesterStatistics(STAT_RECOVERY_FACTOR);        // recovery factor
   data[4]=::TesterStatistics(STAT_TRADES);                 // number of trades
   data[5]=::TesterStatistics(STAT_DEALS);                  // number of deals
   data[6]=::TesterStatistics(STAT_EQUITY_DDREL_PERCENT);   // maximum drawdown of funds as percentage
   data[7]=OnTesterValue;                                   // value of user optimization criterion
   if(data[2]==DBL_MAX)
      data[2]=0;
//--- Create data frame and send it to terminal
   if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME), FRAME_ID, deals_total, data))
      ::PrintFormat("%s: Frame add error: ",__FUNCTION__, ::GetLastError());
  }

When the Expert Advisor receives a frame sent from the agent in the client terminal, a TesterPass event is generated, which is handled in the OnTesterPass() handler.

In this handler, take information from the frame, draw a graph of the balance of this pass on the chart, and fill in the tables of test results and parameters. Save the handled frame in a new frame object and add it to a list of frames for working with it when it is necessary to search for the necessary passes to print them on charts.

Handler OnTesterPass:

//+------------------------------------------------------------------+
//| Retrieves data frame during optimization and prints chart     |    
//| It must be called in Expert Advisor in OnTesterPass() handler        |
//+------------------------------------------------------------------+
void CFrameViewer::OnTesterPass(void)
  {
//--- Variables to work with frames
   string name;
   ulong  pass;
   long   id;
   double value, data[];
   string params[];
   uint   par_count;
   
//--- Auxiliary variables
   static datetime start=::TimeLocal();
   static int      frame_counter=0;

//--- When receive new frame, we receive data from it
   while(!::IsStopped() && ::FrameNext(pass, name, id, value, data))
     {
      frame_counter++;
      string text=::StringFormat("Frames completed (tester passes): %d in %s", frame_counter,::TimeToString(::TimeLocal()-start, TIME_MINUTES|TIME_SECONDS));
      //--- Get input parameters of Expert Advisor, for which frame was formed, and send them to tables and on chart
      //--- Upon successful retrieval of frame write its data to frame object and locate it in list
      if(this.DrawFrameData(0, text, clrNONE, 0, pass, params, par_count, data))
        {
         //--- Results of tester's pass
         double sharpe_ratio=data[0];
         double net_profit=data[1];
         double profit_factor=data[2];
         double recovery_factor=data[3];
         
         //--- Create new frame object and save it in list
         CFrameData *frame=new CFrameData(pass, sharpe_ratio, net_profit, profit_factor, recovery_factor);
         if(frame!=NULL)
           {
            if(!this.AddFrame(frame))
               delete frame;
           }
         ::ChartRedraw();
        }
     }
  }

After the optimization process is completed, the Expert Advisor running in frame mode will remain working in the terminal on the floating chart. And all work with this Expert Advisor will be organized inside OnChartEvent() handler, since we will control the processes we need using buttons on the chart and the mouse cursor.

Handler OnChartEvent:

//+------------------------------------------------------------------+
//|  Event handling on chart                                         |
//+------------------------------------------------------------------+
void CFrameViewer::OnChartEvent(const int id,const long &lparam,
                                   const double &dparam,const string &sparam,
                                   const int delay_ms)
  {
//--- Call event handlers for the tab control object and optimization result charts
   this.m_tab_control.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_0.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_1.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_2.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_3.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_4.OnChartEvent(id, lparam, dparam, sparam);
   
//--- If chart change event has arrived
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- get chart size
      int w=(int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      int h=(int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
      if(w!=this.m_w || h!=this.m_h)
        {
         if(w==0 || h==0)
            return;
         
         //--- Change size of Tab Control
         this.m_tab_control.Resize(w, h);
         
         //--- Get ID of selected tab and draw data tables and optimization chart on tab
         int tab_selected=this.m_tab_control.GetSelectedTabID();
         this.DrawDataChart(tab_selected);
         
         //--- Get pointer to toggle button and selected button for printing optimization results
         CButtonSwitch *button_switch=(tab_selected>0 ? this.GetChartStats(tab_selected).ButtonResult() : NULL);
         uint res_index=(button_switch!=NULL ? button_switch.SelectedButton() : -1);
         
         //--- Depending on selected tab
         switch(tab_selected)
           {
            //--- tab 0 (Optimization)
            case 0 :
               //--- Draw chart with line of last pass and two empty tables
               this.DrawDataChart(0);
               //--- It starts replay of performed optimization,
               //--- which stops working with the rest while replay goes on
               //if(this.m_completed)
               //   this.ReplayFrames(1);
              break;
            
            //--- tabs 1 - 4
            default:
               //--- Retrieve index of selected optimization pass button
               res_index=button_switch.SelectedButton();
               //--- Draw chart with results of three best passes of selected tab
               this.DrawDataChart(tab_selected);
               this.DrawBestFrameData(tab_selected, -1);
               this.DrawBestFrameData(tab_selected, res_index);
               
               //--- On tab 0 draw chart with line of last pass and two empty tables
               this.DrawDataChart(0);
               //--- It starts replay of performed optimization,
               //--- which stops working with the rest while replay goes on
               //--- To re-draw charts of all passes, you can click replay button
               //if(this.m_completed)
               //   this.ReplayFrames(1);
              break;
           }
         
         //--- Remember new dimensions for later verification
         this.m_w=w;
         this.m_h=h;
        }
     }
   
//--- If optimization process is not completed, exit
   if(!this.m_completed)
      return;
      
//--- If custom event has arrived
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- If Replay button event has arrived and optimization is complete
      if(sparam==this.m_chart_stat_0.ButtonReplay().Name() && this.m_completed)
        {
         //--- hide Replay button,
         this.m_chart_stat_0.ButtonReplay().Hide();
         
         //--- Initialize chart of optimization results,
         this.ChartOptDraw(0, this.m_completed, true);
         
         //--- start replay,
         this.m_completed=false;       // block it so as not to run it several times in a row
         this.ReplayFrames(delay_ms);  // replay procedure
         this.m_completed=true;        // unlock   
         
         //--- After replay is complete, show Replay button and redraw chart
         this.m_chart_stat_0.ButtonReplay().Show();
         ::ChartRedraw();
        }
      
      //--- Get pointers to tab buttons
      CTabButton *tab_btn0=this.m_tab_control.GetTabButton(0);
      CTabButton *tab_btn1=this.m_tab_control.GetTabButton(1);
      CTabButton *tab_btn2=this.m_tab_control.GetTabButton(2);
      CTabButton *tab_btn3=this.m_tab_control.GetTabButton(3);
      CTabButton *tab_btn4=this.m_tab_control.GetTabButton(4);
      if(tab_btn0==NULL || tab_btn1==NULL || tab_btn2==NULL || tab_btn3==NULL || tab_btn4==NULL)
         return;
         
      //--- Get ID of selected tab
      int tab_selected=this.m_tab_control.GetSelectedTabID();
      
      //--- If switch event has arrived to tab 0
      if(sparam==tab_btn0.Name())
        {
         //--- On tab 0, draw chart with last pass line and two tables with empty results
         this.DrawDataChart(0);
         //--- It starts replay of performed optimization
         //--- (it can take a long time - if desired, you can click Replay button to print charts)
         //if(this.m_completed)
         //   this.ReplayFrames(1);
         ::ChartRedraw();
         return;
        }
      
      //--- Get pointer to chart of selected tab
      CStatChart *chart_stat=this.GetChartStats(tab_selected);
      if(tab_selected==0 || chart_stat==NULL)
         return;
      //--- Get pointers to chart buttons of selected tab (tab index 1-4)
      CButtonTriggered *button_min=chart_stat.ButtonResultMin();
      CButtonTriggered *button_mid=chart_stat.ButtonResultMid();
      CButtonTriggered *button_max=chart_stat.ButtonResultMax();
      if(button_min==NULL || button_mid==NULL || button_max==NULL)
         return;

      //--- If switch event has arrived to tab 1
      if(sparam==tab_btn1.Name())
        {
         //--- call handler for switching to tab
         this.OnTabSwitchEvent(1);
        }
      //--- If switch event has arrived to tab 2
      if(sparam==tab_btn2.Name())
        {
         //--- call handler for switching to tab
         this.OnTabSwitchEvent(2);
        }
      //--- If switch event has arrived to tab 3
      if(sparam==tab_btn3.Name())
        {
         //--- call handler for switching to tab
         this.OnTabSwitchEvent(3);
        }
      //--- If switch event has arrived to tab 4
      if(sparam==tab_btn4.Name())
        {
         //--- call handler for switching to tab
         this.OnTabSwitchEvent(4);
        }
        
      //--- If event has arrived click on minimum result button of selected tab
      if(sparam==button_min.Name())
        {
         //--- call handler for toggle button switching 
         this.OnButtonSwitchEvent(tab_selected, 0);
        }
      //--- If event has arrived click on mid result button of selected tab
      if(sparam==button_mid.Name())
        {
         //--- call handler for toggle button switching 
         this.OnButtonSwitchEvent(tab_selected, 1);
        }
      //--- If event has arrived click on max result button of selected tab
      if(sparam==button_max.Name())
        {
         //--- call handler for toggle button switching 
         this.OnButtonSwitchEvent(tab_selected, 2);
        }
     }
  }

The tab switching events of the tab control and the click of the toggle buttons are handled in the corresponding custom handlers. All the actions performed in them are identical. The only difference is in tab ID. Therefore, these events are designed to be handled in their own handlers.

Tab switching handler:

//+------------------------------------------------------------------+
//| ▸Tab switching handler                                           |
//+------------------------------------------------------------------+
void CFrameViewer::OnTabSwitchEvent(const int tab_id)
  {
   //--- Get pointer to chart of selected tab
   CStatChart *chart_stat=this.GetChartStats(tab_id);
   if(chart_stat==NULL)
      return;
   
   //--- Get pointer to chart toggle button of selected tab
   CButtonSwitch *button_switch=chart_stat.ButtonResult();
   if(button_switch==NULL)
      return;

   //--- Index of pressed button
   uint butt_index=button_switch.SelectedButton();
   //--- Initialize chart of results on tab_id and
   this.DrawDataChart(tab_id);
   //--- call method that controls display of control elements on all tabs
   this.ControlObjectsView(tab_id);
   
   //--- Draw all three best passes
   this.DrawBestFrameData(tab_id, -1);
   //--- Highlight pass selected by button
   this.DrawBestFrameData(tab_id, butt_index);
  }

Handler for toggle button switching:

//+------------------------------------------------------------------+
//| Handler for toggle button switching                              |
//+------------------------------------------------------------------+
void CFrameViewer::OnButtonSwitchEvent(const int tab_id, const uint butt_id)
  {
   //--- Initialize chart of results on tab_id
   this.DrawDataChart(tab_id);
   //--- Draw all three best passes
   this.DrawBestFrameData(tab_id, -1);
   //--- Highlight pass selected by butt_id button
   this.DrawBestFrameData(tab_id, butt_id);
  }

A method that draws data tables and optimization chart:

//+------------------------------------------------------------------+
//| Draws data tables and optimization chart                       |
//+------------------------------------------------------------------+
void CFrameViewer::DrawDataChart(const uint tab_id)
  {
//--- Draw table of statistics, table of input parameters, and optimization chart
   this.TableStatDraw(tab_id, 4, 4, CELL_W*2, CELL_H, false);
   this.TableInpDraw(tab_id, 4, this.GetTableStats(tab_id).Y2()+4, CELL_W*2, CELL_H, this.GetTableInputs(tab_id).RowsTotal(), false);
   this.ChartOptDraw(tab_id, this.m_completed, true);
   //--- call method that controls display of control elements on all tabs
   this.ControlObjectsView(tab_id);
  }

After all the tables and charts are drawn, it is necessary to arrange the controls correctly. Hide buttons on inactive tabs and display buttons on the active one. This is implemented by ControlObjectsView method.

A method that controls the display of controls on optimization charts:

//+-------------------------------------------------------------------+
//|Controls view of control objects on optimization charts            |
//+-------------------------------------------------------------------+
void CFrameViewer::ControlObjectsView(const uint tab_id)
  {
//--- Get index of active tab
   int tab_index=this.m_tab_control.GetSelectedTabID();
      
//--- Get pointer to active tab and optimization statistics table
   CTab              *tab=this.m_tab_control.GetTab(tab_index);
   CTableDataControl *table_stat=this.GetTableStats(tab_index);

   if(tab==NULL || table_stat==NULL)
      return;
   
//--- Coordinates of left and right boundaries of header of optimization results chart
   int w=0, cpx=0, x=0, y=0;
   int x1=table_stat.X2()+10;
   int x2=tab.GetField().Right()-10;
   
//--- Depending on selected tab index
   switch(tab_index)
     {
      //--- Optimization
      case 0 :
         //--- Shift Replay button to center of header
         w=this.m_chart_stat_0.ButtonReplay().Width();
         cpx=(x1+x2)/2;
         x=cpx-w/2;
         this.m_chart_stat_0.ButtonReplay().MoveX(x);
         
         //--- If optimization is completed, show button in foreground
         if(this.m_completed)
           {
            this.m_chart_stat_0.ButtonReplay().Show();
            this.m_chart_stat_0.ButtonReplay().BringToTop();
           }
         
         //--- Hide buttons of all other tabs
         this.m_chart_stat_1.ButtonsResultHide();
         this.m_chart_stat_2.ButtonsResultHide();
         this.m_chart_stat_3.ButtonsResultHide();
         this.m_chart_stat_4.ButtonsResultHide();
        break;
      
      //--- Sharpe Ratio
      case 1 :
         //--- Hide Replay button
         this.m_chart_stat_0.ButtonReplay().Hide();
         
         //--- Get Y coordinate and move toggle button to it
         y=this.m_chart_stat_1.ProgressBarY1()+CELL_H+2;
         this.m_chart_stat_1.ButtonResult().MoveY(y);
         
         //--- Move toggle button on tab 1 to foreground,
         //--- and hide all other buttons on other tabs
         this.m_chart_stat_1.ButtonsResultBringToTop();
         this.m_chart_stat_2.ButtonsResultHide();
         this.m_chart_stat_3.ButtonsResultHide();
         this.m_chart_stat_4.ButtonsResultHide();
        break;
      
      //--- Net Profit
      case 2 :
         this.m_chart_stat_0.ButtonReplay().Hide();
         
         //--- Get Y coordinate and move toggle button to it
         y=this.m_chart_stat_2.ProgressBarY1()+CELL_H+2;
         this.m_chart_stat_2.ButtonResult().MoveY(y);
         
         //--- Move toggle button on tab 2 to foreground, 
         //--- and hide all other buttons on other tabs
         this.m_chart_stat_2.ButtonsResultBringToTop();
         this.m_chart_stat_1.ButtonsResultHide();
         this.m_chart_stat_3.ButtonsResultHide();
         this.m_chart_stat_4.ButtonsResultHide();
        break;
      
      //--- Profit Factor
      case 3 :
         this.m_chart_stat_0.ButtonReplay().Hide();
         
         //--- Get Y coordinate and move toggle button to it
         y=this.m_chart_stat_3.ProgressBarY1()+CELL_H+2;
         this.m_chart_stat_3.ButtonResult().MoveY(y);
         
         //--- Move toggle button on tab 3 to foreground, 
         //--- and hide all other buttons on other tabs
         this.m_chart_stat_3.ButtonsResultBringToTop();
         this.m_chart_stat_1.ButtonsResultHide();
         this.m_chart_stat_2.ButtonsResultHide();
         this.m_chart_stat_4.ButtonsResultHide();
        break;
      
      //--- Recovery Factor
      case 4 :
         this.m_chart_stat_0.ButtonReplay().Hide();
         
         //--- Get Y coordinate and move toggle button to it
         y=this.m_chart_stat_4.ProgressBarY1()+CELL_H+2;
         this.m_chart_stat_4.ButtonResult().MoveY(y);
         
         //--- Move toggle button on tab 4 to foreground, 
         //--- and hide all other buttons on other tabs
         this.m_chart_stat_4.ButtonsResultBringToTop();
         this.m_chart_stat_1.ButtonsResultHide();
         this.m_chart_stat_2.ButtonsResultHide();
         this.m_chart_stat_3.ButtonsResultHide();
        break;
      default:
        break;
     }
//--- Redraw chart
   ::ChartRedraw();
  }

A method that replays frames after optimization is complete:

//+------------------------------------------------------------------+
//| Replaying frames after optimization completion                   |
//+------------------------------------------------------------------+
void CFrameViewer::ReplayFrames(const int delay_ms)
  {
//--- Variables to work with frames
   string name;
   ulong  pass;
   long   id;
   double value, data[];
   string params[];
   uint   par_count;
   
//--- Frame counter
   int frame_counter=0;
   
//--- Reset progress bar counters
   this.m_progress_bar.Reset();
   this.m_progress_bar.Update(false);
   
//--- Move frame pointer to the beginning and start iterating through frames
   ::FrameFirst();
   while(!::IsStopped() && ::FrameNext(pass, name, id, value, data))
     {
      //--- Increase frame counter and prepare header text of optimization chart
      frame_counter++;
      string text=::StringFormat("Playing with pause %d ms: frame %d", delay_ms, frame_counter);
      //--- Get input parameters of Expert Advisor, for which frame is formed, frame data, and print them on chart
      if(this.DrawFrameData(0, text, clrNONE, 0, pass, params, par_count, data))
         ::ChartRedraw();
      
      //--- Wait for delay_ms milliseconds
      ::Sleep(delay_ms);
     }
  }

All received frames are available for viewing after optimization. Here, in a simple loop, from the very first frame, we move through all the available frames and print their data in tables and on the chart.

A method that prints data of the specified frame on optimization chart:

//+------------------------------------------------------------------+
//| Prints data of specified frame on optimization chart             |
//+------------------------------------------------------------------+
bool CFrameViewer::DrawFrameDataByPass(const uint tab_id, const ulong pass_num, const string text, color clr, const uint line_width, double &data[])
  {
//--- Variables to work with frames
   string name;
   ulong  pass;
   long   id;
   uint   par_count;
   double value;
   string params[];
   
//--- Move frame pointer to the beginning and start search of pass_num frame
   ::FrameFirst();
   while(::FrameNext(pass, name, id, value, data))
     {
      //--- If passe number matches the desired one -
      //--- get frame data and print it in table
      //--- and on chart on tab tab_id
      if(pass==pass_num)
        {
         if(DrawFrameData(tab_id, text, clr, line_width, pass, params, par_count, data))
            return true;
        }
     }
//--- Pass not found
   return false;
  }

Since the frames available after optimization can only be retrieved in a loop of iterating through FrameFirst() --> FrameNext(), and using standard methods, here we loop through all available frames in search of the one which pass number we want. As soon as the desired frame is found, its data is printed on the chart.

Basically, after optimization, we have a ready-made list of frame objects, and we can quickly get the desired object from the list. You can use such access to the desired frame, but in this case you will have to implement more methods to retrieve data from the frame object and the series array, convert them to the desired format and print them on the chart. But for now, access has been left exactly as presented in the method above, in order to reduce the amount of code in the class and simplify its understanding.

A method that draws charts of top three passes by optimization criterion:

//+------------------------------------------------------------------+
//| Draws charts of top three passes by optimization criterion       |
//+------------------------------------------------------------------+
void CFrameViewer::DrawBestFrameData(const uint tab_id, const int res_index)
  {
//--- If incorrect identifiers of table and pressed button are passed, exit
   if(tab_id<1 || tab_id>4 || res_index>2)
     {
      ::PrintFormat("%s: Error. Incorrect table (%u) or selected button (%d) identifiers passed",__FUNCTION__, tab_id, res_index);
      return;
     }
      
//--- Arrays for getting results of passes
   ulong array_passes[3];
   double data[];
   
//--- Create header text of pass chart
   string res=
     (
      tab_id==1 ? "Results by Sharpe Ratio"     : 
      tab_id==2 ? "Results by Net Profit"       :
      tab_id==3 ? "Results by Profit Factor"    :
      tab_id==4 ? "Results by Recovery Factor"  :
      ""
     );
   string text="Optimization Completed: "+res;
   
//--- Fill in array_passes array by indexes of three best passes
   this.FillArrayBestFrames(tab_id, array_passes);
//--- If index of pass button is set to negative number -
   if(res_index<0)
     {
      //--- print all three passes on chart
      //--- (line color is specified as clrNONE for automatic line color selection of profitable or loss series)
      for(int i=0; i<(int)array_passes.Size(); i++)
         this.DrawFrameDataByPass(tab_id, array_passes[i], text, clrNONE, 0, data);
     }
   //--- Otherwise, print series indicated by index of pressed button on chart (res_index),
   //--- by color set in m_selected_color, and width specified in m_line_width
   else
      this.DrawFrameDataByPass(tab_id, array_passes[res_index], text, this.m_selected_color, this.m_line_width, data);
  }

Here, the array is first filled with the frame indexes of the top three passes in the FillArrayBestFrames() method, and then the desired pass (or all three of them) is printed on the chart.

A method that fills an array with frame indexes of the top three passes for the specified optimization criterion:

//+------------------------------------------------------------------+
//| Fills array with frame indexes of top three passes          |
//| for specified optimization criterion (by tab index)         |
//+------------------------------------------------------------------+
bool CFrameViewer::FillArrayBestFrames(const uint tab_id, ulong &array_passes[])
  {
//--- Clear array of optimization pass indexes passed to method
   ::ZeroMemory(array_passes);
   
   //FRAME_PROP_PASS_NUM,           // Pass number
   //FRAME_PROP_SHARPE_RATIO,       // Sharpe Ratio result
   //FRAME_PROP_NET_PROFIT,         // Net Profit result
   //FRAME_PROP_PROFIT_FACTOR,      // Profit Factor result
   //FRAME_PROP_RECOVERY_FACTOR,    // Recovery Factor result
   
//--- By using tab ID, determine property by which best optimization passes will be searched
//--- Check tab ID to be within 1 to 4
   if(tab_id<FRAME_PROP_SHARPE_RATIO || tab_id>FRAME_PROP_RECOVERY_FACTOR)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id);
      return false;
     }
   
//--- Convert table ID to frame property
   ENUM_FRAME_PROP prop=(ENUM_FRAME_PROP)tab_id;
   
//--- Sort frame list in ascending order by property,
//--- which corresponds to tab_id value as ENUM_FRAME_PROP
   this.m_list_frames.Sort(prop);

//--- After sorting, frame with best result will be at list end
//--- Using index, get frame from list with maximum result value and
   int index=this.m_list_frames.Total()-1;
   CFrameData *frame_next=this.m_list_frames.At(index);
   if(frame_next==NULL)
      return false;
      
//--- register pass number to last cell of array_passes
   array_passes[2]=frame_next.Pass();
   
//--- Now find objects for which optimization result in descending order is less than maximum found
//--- In loop from 1 to 0 (remaining cells of array_passes)
   for(int i=1; i>=0; i--)
     {
      //--- look for previous object with property value less than that of frame_next object
      frame_next=this.FrameSearchLess(frame_next, prop);
      //--- In next cell of array_passes, enter pass number of found object
      //--- If object is not found, it means that there are no objects with value less than that of frame_next object,
      //--- and in this case, enter its previous value to next cell of array_passes
      array_passes[i]=(frame_next!=NULL ? frame_next.Pass() : array_passes[i+1]);
     }
//--- Success
   return true;
  }

The entire method's logic is fully explained in comments to the code. At the end of method run in the array, the numbers of the top three passes will be recorded with size 3 by the optimization criterion corresponding to the tab number, on which chart the data of these passes must be printed. The FrameSearchLess() method is used to search for frames with a property value lower than that of the current one.

A method for searching for and returning a pointer to a frame object with a property value less than the sample:

//+------------------------------------------------------------------+
//| Searches for and returns pointer to frame object,                |
//| with property value less than sample                             |
//+------------------------------------------------------------------+
CFrameData *CFrameViewer::FrameSearchLess(CFrameData *frame, const int mode)
  {
//--- Depending on type of frame property
   switch(mode)
     {
      //--- to temporary object record corresponding property of object passed to method
      case FRAME_PROP_SHARPE_RATIO     :  this.m_frame_tmp.SetSharpeRatio(frame.SharpeRatio());       break;
      case FRAME_PROP_NET_PROFIT       :  this.m_frame_tmp.SetNetProfit(frame.NetProfit());           break;
      case FRAME_PROP_PROFIT_FACTOR    :  this.m_frame_tmp.SetProfitFactor(frame.ProfitFactor());     break;
      case FRAME_PROP_RECOVERY_FACTOR  :  this.m_frame_tmp.SetRecoveryFactor(frame.RecoveryFactor()); break;
      default                          :  this.m_frame_tmp.SetPass(frame.Pass());                     break;
     }
//--- Sort array of frames by specified property and
   this.m_list_frames.Sort(mode);
//--- get index of nearest object with lower property value, or -1
   int index=this.m_list_frames.SearchLess(&this.m_frame_tmp);
//--- Get object by index from list and return pointer to it, or NULL
   CFrameData *obj=this.m_list_frames.At(index);
   return obj;
  }

A frame is passed to the method, and in the sorted list of frames, SearchLess() method of CArrayObj class of the Standard Library is used to search for the nearest object whose property value is less than that of the one passed to the method.

A method that prints three best passes on optimization results charts on each tab

//+------------------------------------------------------------------+
//| Prints on optimization results charts                            |
//| on each tab three best passes                                    |
//+------------------------------------------------------------------+
void CFrameViewer::DrawBestFrameDataAll(void)
  {
//--- In a loop through all tabs from tab 1, draw charts of top three passes for each tab
   for(int i=1; i<this.m_tab_control.TabsTotal(); i++)
      this.DrawBestFrameData(i,-1);
  }

A method for retrieving data of current frame and printing it on specified tab in table and on optimization results chart:

//+------------------------------------------------------------------+
//| Retrieving data of current frame and printing it on specified tab |
//| in table and on optimization results chart                    |
//+------------------------------------------------------------------+
bool CFrameViewer::DrawFrameData(const uint tab_id, const string text, color clr, const uint line_width, ulong &pass, string &params[], uint &par_count, double &data[])
  {
//--- Check passed tab ID
   if(tab_id>4)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id);
      return false;
     }
//--- Get pointers to objects used on specified tab
   CCanvas           *foreground=this.m_tab_control.GetTabForeground(tab_id);
   CTableDataControl *table_stat=this.GetTableStats(tab_id);
   CTableDataControl *table_inp=this.GetTableInputs(tab_id);
   CStatChart        *chart_stat=this.GetChartStats(tab_id);

   if(foreground==NULL || table_stat==NULL || table_inp==NULL || chart_stat==NULL)
      return false;
      
//--- Get input parameters of Expert Advisor, for which frame is formed, frame data, and print them on chart
   ::ResetLastError();
   if(::FrameInputs(pass, params, par_count))
     {
      //--- Draw table of input parameters on chart
      this.TableInpDraw(tab_id, 4, table_stat.Y2()+4, CELL_W*2, CELL_H, par_count, false);
      
      //--- Iterate through parameters, params[i], string looks as "parameter=value"
      for(uint i=0; i<par_count; i++)
        {
         //--- Fill in table with names and values of input parameters
         string array[];
         //--- Split string in params[i] into two substrings and update cells in string of test parameters table
         if(::StringSplit(params[i],'=',array)==2)
           {
            //--- Fill in strings of optimized parameters with pale yellow color,
            //--- parameters that are not available for optimization - to pale pink, the rest - to default colors
            bool enable=false;
            double value=0, start=0, step=0, stop=0;
            color clr=clrMistyRose;
            if(::ParameterGetRange(array[0], enable, value, start, step, stop))
               clr=(enable ? clrLightYellow : clrNONE);
            
            //--- Get two cells of table by parameter index and print text of parameter name and its value to them
            CTableCell *cell_0=table_inp.GetCell(i, 0);
            CTableCell *cell_1=table_inp.GetCell(i, 1);
            if(cell_0!=NULL && cell_1!=NULL)
              {
               //--- Update captions in cells
               cell_0.SetText(array[0]);
               cell_1.SetText(array[1]);
               cell_0.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER);
               cell_1.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER);
              }
           }
        }
      
      //--- Update optimization statistics table
      //--- Table header string
      foreground.FillRectangle(table_stat.X1()+1, 4+1, table_stat.X1()+CELL_W*2-1, 4+CELL_H-1, 0x00FFFFFF);
      foreground.FontSet("Calibri", -100, FW_BLACK);
      foreground.TextOut(4+(CELL_W*2)/2, 4+CELL_H/2, ::StringFormat("Optimization results (pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
      
      //--- In loop by number of strings in table
      int total=table_stat.RowsTotal();
      for(int i=0; i<total; i++)
        {
         //--- get two cells of current string and
         CTableCell *cell_0=table_stat.GetCell(i, 0);
         CTableCell *cell_1=table_stat.GetCell(i, 1);
         if(cell_0!=NULL && cell_1!=NULL)
           {
            //--- update values of pass results in second cell
            string text="---";
            switch(i)
              {
               case 0 : text=::StringFormat("%.2f", data[0]);  break;   // Sharpe Ratio
               case 1 : text=::StringFormat("%.2f", data[1]);  break;   // Net Profit
               case 2 : text=::StringFormat("%.2f", data[2]);  break;   // Profit Factor
               case 3 : text=::StringFormat("%.2f", data[3]);  break;   // Recovery Factor
               case 4 : text=::StringFormat("%.0f", data[4]);  break;   // Trades
               case 5 : text=::StringFormat("%.0f", data[5]);  break;   // Deals
               case 6 : text=::StringFormat("%.2f%%", data[6]);break;   // Equity DD
               case 7 : text=::StringFormat("%G", data[7]);    break;   // OnTester()
               default: break;
              }
            //--- Highlight background of table string corresponding to selected tab with color.
            //--- Remaining strings will have default color
            color clr=(tab_id>0 ? (i==tab_id-1 ? C'223,242,231' : clrNONE) : clrNONE);
            
            //--- Update captions in cells
            cell_0.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER);
            cell_1.SetText(text);
            cell_1.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER);
           }
        }
      
      //--- Array for accepting values of balance of current frame
      double seria[];
      ::ArrayCopy(seria, data, 0, DATA_COUNT, ::ArraySize(data)-DATA_COUNT);
      //--- Send array for printing on special balance chart
      chart_stat.AddSeria(seria, data[1]>0);
      //--- Update balance lines on chart 
      chart_stat.Update(clr, line_width, false);
      //--- Update progress bar (only for tab with ID 0)
      if(tab_id==0)
         this.m_progress_bar.AddResult(data[1]>0, false);
      
      //--- Update caption on chart header
      int x1=chart_stat.HeaderX1();
      int y1=chart_stat.HeaderY1();
      int x2=chart_stat.HeaderX2();
      int y2=chart_stat.HeaderY2();
      
      int x=(x1+x2)/2;
      int y=(y1+y2)/2;
      
      foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
      foreground.FontSet("Calibri", -100, FW_BLACK);
      foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
      foreground.Update(false);
      
      //--- Success
      return true;
     }
//--- Failed...
   else
      PrintFormat("%s: FrameInputs() failed. Error %d",__FUNCTION__, ::GetLastError());
   return false;
  }

In the method, data is retrieved from the frame, all tables are filled with this data, and a chart of the balance of this optimization pass is drawn.

A method that draws a table of optimization statistics on the specified tab:

//+------------------------------------------------------------------+
//| Draws table of optimization statistics on specified tab          |
//+------------------------------------------------------------------+
void CFrameViewer::TableStatDraw(const uint tab_id, const int x, const int y, const int w, const int h, const bool chart_redraw)
  {
//--- Check passed tab ID
   if(tab_id>4)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id);
      return;
     }
//--- Get pointers to objects used on specified tab
   CCanvas           *background=this.m_tab_control.GetTabBackground(tab_id);
   CCanvas           *foreground=this.m_tab_control.GetTabForeground(tab_id);
   CTableDataControl *table_stat=this.GetTableStats(tab_id);
   
   if(background==NULL || foreground==NULL || table_stat==NULL)
      return;
   
//--- Draw header of optimization results table
   background.FillRectangle(x, y, x+CELL_W*2, y+CELL_H, ::ColorToARGB(C'195,209,223'));   // C'180,190,230'
   foreground.FillRectangle(x+1, y+1, x+CELL_W*2-1, y+CELL_H-1, 0x00FFFFFF);
   foreground.FontSet("Calibri", -100, FW_BLACK);
   foreground.TextOut(x+(CELL_W*2)/2, y+CELL_H/2, "Optimization results", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
   
//--- Set table's identifier and draw table grid
   table_stat.SetID(TABLE_OPT_STAT_ID+10*tab_id);
   table_stat.DrawGrid(background, x, y+CELL_H, 0, DATA_COUNT, 2, CELL_H, CELL_W, C'200,200,200', false);
   
//--- Draw empty table of optimization results - only headers, without values
//--- In a loop by table rows
   int total=table_stat.RowsTotal();
   for(int row=0; row<total; row++)
     {
      //--- iterate through columns of rows
      for(int col=0; col<2; col++)
        {
         //--- Get table cell in current row and column
         CTableCell *cell=table_stat.GetCell(row, col);
         //--- Define text in cell
         //--- For left cell, these will be headers of results of parameters optimized
         if(col%2==0)
           {
            string text="OnTester()";
            switch(row)
              {
               case 0 : text="Sharpe Ratio";    break;
               case 1 : text="Net Profit";      break;
               case 2 : text="Profit Factor";   break;
               case 3 : text="Recovery Factor"; break;
               case 4 : text="Trades";          break;
               case 5 : text="Deals";           break;
               case 6 : text="Equity DD";       break;
               default: break;
              }
            cell.SetText(text);
           }
         //--- For right cell, text will be strikeout for table initialized
         else
            cell.SetText(tab_id==0 ? " --- " : "");
         
         //--- Print corresponding text in cell
         cell.TextOut(foreground, 4, CELL_H/2, clrNONE, 0, TA_VCENTER);
        }
     }
   
//--- Update background and foreground canvas
   background.Update(false);
   foreground.Update(chart_redraw);
  }

The method draws a table of optimization results, filling in only the row headers of the table. The data cells are entered into the table using the above described method.

A method that draws a table of input optimization parameters on the specified tab:

//+------------------------------------------------------------------+
//|Draws table of input optimization parameters on the specified tab |
//+------------------------------------------------------------------+
void CFrameViewer::TableInpDraw(const uint tab_id, const int x, const int y, const int w, const int h, const uint rows, const bool chart_redraw)
  {
//--- Check passed tab ID
   if(tab_id>4)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id);
      return;
     }
//--- Get pointers to objects used on specified tab
   CCanvas           *background=this.m_tab_control.GetTabBackground(tab_id);
   CCanvas           *foreground=this.m_tab_control.GetTabForeground(tab_id);
   CTableDataControl *table_inp=this.GetTableInputs(tab_id);
   
   if(background==NULL || foreground==NULL || table_inp==NULL)
      return;
   
//--- Draw header of optimization parameters table
   background.FillRectangle(x, y, x+CELL_W*2, y+CELL_H, ::ColorToARGB(C'195,209,223'));
   foreground.FillRectangle(x+1, y+1, x+CELL_W*2-1, y+CELL_H-1, 0x00FFFFFF);
   foreground.FontSet("Calibri", -100, FW_BLACK);
   foreground.TextOut(x+(CELL_W*2)/2, y+CELL_H/2, "Input parameters", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
   
//--- Set table's identifier and draw table grid
   table_inp.SetID(TABLE_OPT_INP_ID+10*tab_id);
   table_inp.DrawGrid(background, x, y+CELL_H, 0, rows, 2, CELL_H, CELL_W, C'200,200,200', false);
   
//--- Update background and foreground canvas
   background.Update(false);
   foreground.Update(chart_redraw);
  }

This method, just like the previous one, draws an empty table of optimization parameters, which is filled with data in the DrawFrameData() method, where the parameters with which a pass of the tester was performed are already known.

A method that draws the optimization chart on the specified tab:

//+------------------------------------------------------------------+
//| Draws chart of optimization on specified tab                     |
//+------------------------------------------------------------------+
void CFrameViewer::ChartOptDraw(const uint tab_id, const bool opt_completed, const bool chart_redraw)
  {
//--- Check passed tab ID
   if(tab_id>4)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id);
      return;
     }
//--- Get pointers to objects used on specified tab
   CCanvas           *background=this.m_tab_control.GetTabBackground(tab_id);
   CCanvas           *foreground=this.m_tab_control.GetTabForeground(tab_id);
   CTab              *tab=this.m_tab_control.GetTab(tab_id);
   CTableDataControl *table_stat=this.GetTableStats(tab_id);
   CStatChart        *chart_stat=this.GetChartStats(tab_id);
   
   if(background==NULL || foreground==NULL || tab==NULL || table_stat==NULL || chart_stat==NULL)
      return;
   
//--- Calculate coordinates of four corners of optimization results chart
   int x1=table_stat.X2()+10;
   int y1=table_stat.Y1();
   int x2=tab.GetField().Right()-10;
   int y2=tab.GetField().Bottom()-tab.GetButton().Height()-12;
   
//--- Check size limits by minimum width and height (480 x 180)
   int w_min=480;
   if(x2-x1<w_min)
      x2=x1+w_min;
   if(y2-y1<180)
      y2=y1+180;
   
//--- Set dimensions of bounding rectangle of optimization results chart
   chart_stat.SetChartBounds(x1, y1, x2, y2);
   
//--- Color and text of chart header
   color clr=clrLightGreen;   // header color at optimization completion
   string suff=
     (
      tab_id==1 ? "Results by Sharpe Ratio"     : 
      tab_id==2 ? "Results by Net Profit"       :
      tab_id==3 ? "Results by Profit Factor"    :
      tab_id==4 ? "Results by Recovery Factor"  :
                     "Click to Replay"
     );
   string text="Optimization Completed: "+suff;
   
//--- If optimization is not completed, specify color and text of header
   if(!opt_completed)
     {
      clr=C'195,209,223';
      text=::StringFormat("Optimization%sprogress%s", (tab_id==0 ? " " : " in "), (tab_id==0 ? "" : ": Waiting ... "));
     }
   
//--- Draw header and text
   background.FillRectangle(x1, 4, x2, y1, ::ColorToARGB(clr));
   foreground.FillRectangle(x1, 4, x2, y2, 0x00FFFFFF);
   foreground.FontSet("Calibri", -100, FW_BLACK);
   foreground.TextOut((x1+x2)/2, 4+CELL_H/2, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
   
//--- Erase whole chart of optimization results
   background.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
   foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
   
//--- Update optimization chart
   chart_stat.Update(clrNONE, 0, chart_redraw);
  }

The method prepares a clean chart with a header, on which balance lines of completed optimization passes are printed from drawing methods.

We have fully implemented all the necessary classes for visual optimization. Now, the CFrameViewer class file can be attached to any Expert Advisor to view the progress of its optimization on a separate chart in the terminal.


Connect functionality to the Expert Advisor

Let’s check what we have.

Take the EA from the standard delivery from the location: \MQL5\Experts\Advisors\ExpertMAMA.mq5 and save it in a new, previously created folder \MQL5\Experts\FrameViewer\ named as ExpertMAMA_Frames.mq5.

All that should be added to it is to connect the CFrameViewer class file at the end of the listing, declare an object with the type of this class and add handlers in which handlers of the same name of the created class must be called.

The length of EA's input variables can be slightly shortened by removing the underscores ("_") from the variable names. This will give them more space to fit into the width of table cells.

//+------------------------------------------------------------------+
//|                                                   ExpertMAMA.mq5 |
//|                             Copyright 2000-2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include                                                          |
//+------------------------------------------------------------------+
#include <Expert\Expert.mqh>
#include <Expert\Signal\SignalMA.mqh>
#include <Expert\Trailing\TrailingMA.mqh>
#include <Expert\Money\MoneyNone.mqh>
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
//--- inputs for expert
input string             InpExpertTitle      =  "ExpertMAMA";
int                      Expert_MagicNumber  =  12003;
bool                     Expert_EveryTick    =  false;
//--- inputs for signal
input int                InpSignalMAPeriod   =  12;
input int                InpSignalMAShift    =  6;
input ENUM_MA_METHOD     InpSignalMAMethod   =  MODE_SMA;
input ENUM_APPLIED_PRICE InpSignalMAApplied  =  PRICE_CLOSE;
//--- inputs for trailing
input int                InpTrailingMAPeriod =  12;
input int                InpTrailingMAShift  =  0;
input ENUM_MA_METHOD     InpTrailingMAMethod =  MODE_SMA;
input ENUM_APPLIED_PRICE InpTrailingMAApplied=  PRICE_CLOSE;
//+------------------------------------------------------------------+
//| Global expert object                                             |
//+------------------------------------------------------------------+
CExpert ExtExpert;
//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Initializing expert
   if(!ExtExpert.Init(Symbol(),Period(),Expert_EveryTick,Expert_MagicNumber))
     {
      //--- failed
      printf(__FUNCTION__+": error initializing expert");
      ExtExpert.Deinit();
      return(-1);
     }
//--- Creation of signal object
   CSignalMA *signal=new CSignalMA;
   if(signal==NULL)
     {
      //--- failed
      printf(__FUNCTION__+": error creating signal");
      ExtExpert.Deinit();
      return(-2);
     }
//--- Add signal to expert (will be deleted automatically))
   if(!ExtExpert.InitSignal(signal))
     {
      //--- failed
      printf(__FUNCTION__+": error initializing signal");
      ExtExpert.Deinit();
      return(-3);
     }
//--- Set signal parameters
   signal.PeriodMA(InpSignalMAPeriod);
   signal.Shift(InpSignalMAShift);
   signal.Method(InpSignalMAMethod);
   signal.Applied(InpSignalMAApplied);
//--- Check signal parameters
   if(!signal.ValidationSettings())
     {
      //--- failed
      printf(__FUNCTION__+": error signal parameters");
      ExtExpert.Deinit();
      return(-4);
     }
//--- Creation of trailing object
   CTrailingMA *trailing=new CTrailingMA;
   if(trailing==NULL)
     {
      //--- failed
      printf(__FUNCTION__+": error creating trailing");
      ExtExpert.Deinit();
      return(-5);
     }
//--- Add trailing to expert (will be deleted automatically))
   if(!ExtExpert.InitTrailing(trailing))
     {
      //--- failed
      printf(__FUNCTION__+": error initializing trailing");
      ExtExpert.Deinit();
      return(-6);
     }
//--- Set trailing parameters
   trailing.Period(InpTrailingMAPeriod);
   trailing.Shift(InpTrailingMAShift);
   trailing.Method(InpTrailingMAMethod);
   trailing.Applied(InpTrailingMAApplied);
//--- Check trailing parameters
   if(!trailing.ValidationSettings())
     {
      //--- failed
      printf(__FUNCTION__+": error trailing parameters");
      ExtExpert.Deinit();
      return(-7);
     }
//--- Creation of money object
   CMoneyNone *money=new CMoneyNone;
   if(money==NULL)
     {
      //--- failed
      printf(__FUNCTION__+": error creating money");
      ExtExpert.Deinit();
      return(-8);
     }
//--- Add money to expert (will be deleted automatically))
   if(!ExtExpert.InitMoney(money))
     {
      //--- failed
      printf(__FUNCTION__+": error initializing money");
      ExtExpert.Deinit();
      return(-9);
     }
//--- Set money parameters
//--- Check money parameters
   if(!money.ValidationSettings())
     {
      //--- failed
      printf(__FUNCTION__+": error money parameters");
      ExtExpert.Deinit();
      return(-10);
     }
//--- Tuning of all necessary indicators
   if(!ExtExpert.InitIndicators())
     {
      //--- failed
      printf(__FUNCTION__+": error initializing indicators");
      ExtExpert.Deinit();
      return(-11);
     }
//--- succeed
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Deinitialization function of the expert                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ExtExpert.Deinit();
  }
//+------------------------------------------------------------------+
//| Function-event handler "tick"                                    |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ExtExpert.OnTick();
  }
//+------------------------------------------------------------------+
//| Function-event handler "trade"                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
   ExtExpert.OnTrade();
  }
//+------------------------------------------------------------------+
//| Function-event handler "timer"                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   ExtExpert.OnTimer();
  }
//+------------------------------------------------------------------+
//| Code required to visualize optimization                    |
//+------------------------------------------------------------------+
//--- When debugging, if press "Stop" during optimization, next run of optimization will continue incomplete passes from stop point
//--- In order for each new optimization run to start anew, define preprocessor directive
#property tester_no_cache

//--- Define macro substitutions
#define   REPLAY_DELAY_MS      100           // Optimization replay delay in milliseconds
#define   STAT_LINES           1             // Number of optimization statistics lines displayed
#define   SELECTED_LINE_WD     3             // Thickness of line of selected optimization passage
#define   SELECTED_LINE_CLR    clrDodgerBlue // Color of line of selected optimization passage

//--- Connect code to work with the optimization results by frame viewer
#include  "FrameViewer.mqh"

//--- Declare frame viewer object
CFrameViewer fw;

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- here insert your own function to calculate optimization criterion
   double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT));
   TesterCritetia=TesterStatistics(STAT_PROFIT)>0?TesterCritetia:(-TesterCritetia);
//--- call at each end of testing and pass optimization criterion as parameter
   fw.OnTester(TesterCritetia);
//---
   return(TesterCritetia);
  }
//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
void OnTesterInit()
  {
//--- prepare chart for displaying balance lines
//--- STAT_LINES sets number of balance lines on chart,
//--- SELECTED_LINE_WD - sets width, SELECTED_LINE_CLR - sets color of line of selected passage
   fw.OnTesterInit(STAT_LINES, SELECTED_LINE_WD, SELECTED_LINE_CLR);
  }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
//--- completing optimization
   fw.OnTesterDeinit();
  }
//+------------------------------------------------------------------+
//| TesterPass function                                              |
//+------------------------------------------------------------------+
void OnTesterPass()
  {
//--- handle test results and display graphics
   fw.OnTesterPass();
  }
//+------------------------------------------------------------------+
//|  Event handling on chart                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- starts playback of frames upon completion of optimization when clicking on header
   fw.OnChartEvent(id,lparam,dparam,sparam,REPLAY_DELAY_MS); // REPLAY_DELAY_MS - pause in ms between replay frames
  }

These are all the changes and additions to the Expert Advisor that must be made (except for shortening variable names) in order for visual optimization to work.

Let's compile the Expert Advisor and run it for optimization.

Optimization settings for the test of the program itself are not particularly important, set them like this:

and run optimization:

Before starting the optimization process, a new chart window opens. All the controls are located on it. This is convenient so that you don't have to switch between the attached charts of optimization results and the chat of visual optimization. You can move this separate window outside the terminal, or onto a second monitor, and simultaneously have access to all optimization charts.


Conclusion

In conclusion, I would like to say that we have considered only a small example of how additional functionality can be implemented to control the optimization process. On the visual optimization chart you can print any data obtained from tester's reports, or independently calculated after each optimization pass. What the functionality and visual display can be is a matter of taste and the needs of each developer who uses visual optimization to achieve the desired results and convenience in using the obtained data. Here, in this situation, it is important that we have discussed at specific examples how you can implement and use everything you need for yourself.

All the files discussed in the article are attached to the article for self-study. Old_article_files.zip contains files from the article, based on which information everything was realised today.

The MQL5.zip archive is also attached. After unpacking it, you can immediately get the installed test files in the necessary terminal folders.

Programs used in the article:

#
Name
Type
 Description
 1  Table.mqh Class Library Class Library for table creation
 2  Controls.mqh Class Library A class library for creating UI controls
 3  FrameViewer.mqh Class Library A class library for implementing visual optimization functionality in the Expert Advisor
 4  ExpertMAMA_Frames.mq5 Expert Advisor Expert Advisor to test visual optimization
 5  MQL5.zip Archive An archive of the files presented above for unpacking into the MQL5 directory of the client terminal
 6  Old_article_files.zip Archive Archive of files from the original article, on the basis of which all files of this article were created

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

Attached files |
Table.mqh (66.62 KB)
Controls.mqh (183.33 KB)
FrameViewer.mqh (187.03 KB)
MQL5.zip (52 KB)
Last comments | Go to discussion (2)
fxsaber
fxsaber | 21 Mar 2025 at 10:32

After the disclosure of the opt-format, the use of frames remained appropriate only when transferring data that is not in the opt-file.

In the example of this article, the proposed GUI could be used to visualise an opt-file.

Artyom Trishkin
Artyom Trishkin | 21 Mar 2025 at 10:53
fxsaber #:

After the disclosure of the opt format, the use of frames remained appropriate only when transmitting data that is not in the opt file.

In the example of this article, the proposed GUI could be used to visualise an opt-file.

Didn't delve that far. Interesting, thanks.
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Database Is Easy (Part 1): A Lightweight ORM Framework for MQL5 Using SQLite Database Is Easy (Part 1): A Lightweight ORM Framework for MQL5 Using SQLite
This article presents a structured way to manage SQLite data in MQL5 through an ORM layer for MetaTrader 5. It introduces core classes for entity modeling and database access, a fluent CRUD API, reflection hooks for OnGet/OnSet, and macros to define models quickly. Practical code shows creating tables, binding fields, inserting, updating, querying, and deleting records. Developers gain reusable, type-safe components that minimize repetitive SQL.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
From Novice to Expert: Statistical Validation of Supply and Demand Zones From Novice to Expert: Statistical Validation of Supply and Demand Zones
Today, we uncover the often overlooked statistical foundation behind supply and demand trading strategies. By combining MQL5 with Python through a Jupyter Notebook workflow, we conduct a structured, data-driven investigation aimed at transforming visual market assumptions into measurable insights. This article covers the complete research process, including data collection, Python-based statistical analysis, algorithm design, testing, and final conclusions. To explore the methodology and findings in detail, read the full article.