preview
Building a Session Performance Analytics Dashboard in MQL5

Building a Session Performance Analytics Dashboard in MQL5

MetaTrader 5Statistics and analysis |
79 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

Trading statistics in MetaTrader read like a smooth, undifferentiated feed: time, symbol, and profit for every deal, but no answer to the question many traders actually want to test, which market hours generate profit and which quietly drag results down. A trader testing the hypothesis that a strategy performs well during the London session but suffers overnight in Sydney gets no ready-made breakdown by session from the terminal. This usually ends in exporting history to a spreadsheet, marking rows by UTC hour by hand, and repeating the same routine for every new period reviewed. What is needed instead is a way to read closed deals for a chosen lookback window, assign each one to a trading session by its close time, and aggregate the metrics so every session's contribution shows on one screen.

This article builds a dashboard that removes that manual step. A script reads the terminal's closed-deal history for a configurable lookback window. It classifies each deal into one of four trading sessions based on its UTC close time, computes per-session metrics (net P&L, win rate, trade count, and average hold time), and renders a color-coded CCanvas bar chart. A parallel summary table goes to the Experts tab. The sections below walk through six include files and a main script function by function, followed by a verification script.

Session dashboard architectural diagram

Architecture diagram: SessionDashboard.mq5 runs the pipeline top to bottom through the history reader, classifier, and aggregator, then fans out to the bar chart and table printer, both of which land in the trader-facing chart panel and Experts tab.



Section 1 — SessionEnums.mqh: Session Boundaries and Data Structures

A trading session is one of four fixed UTC windows: Sydney, Tokyo, London, and New York. UTC is used as the reference frame rather than the broker's own server time, since server time shifts with daylight saving rules that differ by broker, while UTC never shifts.

ENUM_TRADING_SESSION names the four sessions plus a SESSION_UNKNOWN fallback for a deal that matches no configured boundary.

//+------------------------------------------------------------------+
//|                                                  SessionEnums.mqh|
//+------------------------------------------------------------------+
#ifndef SESSIONENUMS_MQH
#define SESSIONENUMS_MQH

//+------------------------------------------------------------------+
//| ENUM_TRADING_SESSION                                             |
//+------------------------------------------------------------------+
enum ENUM_TRADING_SESSION
  {
   SESSION_SYDNEY = 0,
   SESSION_TOKYO,
   SESSION_LONDON,
   SESSION_NEW_YORK,
   SESSION_UNKNOWN
  };

CSessionBoundary describes one session's identity: which session it is, its display name, its UTC open and close hour, and the bar color used when it is profitable. Sydney's open hour (22) is larger than its close hour (7); that mismatch is the signal the classifier later uses to detect a boundary spanning midnight.

//+------------------------------------------------------------------+
//| CSessionBoundary                                                 |
//| Describes one trading session's UTC open and close hour, its     |
//| display name, and the color used to draw a profitable bar for    |
//| that session.                                                    |
//+------------------------------------------------------------------+
struct CSessionBoundary
  {
   ENUM_TRADING_SESSION session_type;
   string               name;
   int                  utc_open_hour;
   int                  utc_close_hour;
   color                bar_color;
  };

CSessionMetrics holds the aggregated numbers for one session: trade count, win count, net P&L, total hold time in seconds, and an average hold time already divided out. Storing both the total and the average means the aggregator only performs that division once.

//+------------------------------------------------------------------+
//| CSessionMetrics                                                  |
//| Holds the computed session metrics for one trading session:      |
//| trade count, win count, net P&L, and hold time totals.           |
//+------------------------------------------------------------------+
struct CSessionMetrics
  {
   ENUM_TRADING_SESSION session_type;
   string               name;
   int                  trade_count;
   int                  win_count;
   double               net_pnl;
   long                 total_hold_time;
   double               average_hold_time;
  };

CDealRecord is the smallest unit the pipeline works with, one closed deal reduced to just the fields the rest of the system needs, including the session_type assigned by the classifier.

//+------------------------------------------------------------------+
//| CDealRecord                                                      |
//| Holds the minimal fields read from one closed deal, including    |
//| the trading session it has been classified into.                 |
//+------------------------------------------------------------------+
struct CDealRecord
  {
   ulong                ticket;
   datetime             open_time;
   datetime             close_time;
   double               profit;
   string               symbol;
   ENUM_TRADING_SESSION session_type;
  };



Section 2 — CDealHistoryReader: Reading Closed Deals

The reader's job is to pull closed deals out of the terminal's history and hand back a clean array of records.

//+------------------------------------------------------------------+
//|                                            DealHistoryReader.mqh |
//+------------------------------------------------------------------+
#ifndef DEALHISTORYREADER_MQH
#define DEALHISTORYREADER_MQH

#include "SessionEnums.mqh"

//+------------------------------------------------------------------+
//| CDealHistoryReader                                               |
//| Reads closed deal history for a date range and populates an      |
//| array of CDealRecord entries, skipping balance and deposit       |
//| deals and recovering each deal's open time from its position.    |
//+------------------------------------------------------------------+
class CDealHistoryReader
  {
private:
   datetime              FindOpenTime(ulong position_id, datetime close_time_utc) const;

public:
                     CDealHistoryReader(void);
                    ~CDealHistoryReader(void);

   int                   Read(datetime from, datetime to, CDealRecord &records[]);
  };

//+------------------------------------------------------------------+
//| Constructor: no member state to initialize.                      |
//+------------------------------------------------------------------+
CDealHistoryReader::CDealHistoryReader(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor: no dynamic resources to release.                     |
//+------------------------------------------------------------------+
CDealHistoryReader::~CDealHistoryReader(void)
  {
  }

Read() starts by scoping the history cache with HistorySelect(), then iterates every deal in that range. Only DEAL_ENTRY_OUT and DEAL_ENTRY_INOUT deals are kept, since these represent an actual closed deal rather than a balance operation or an opening deal.

//+------------------------------------------------------------------+
//| Read                                                             |
//+------------------------------------------------------------------+
int CDealHistoryReader::Read(datetime from, datetime to, CDealRecord &records[])
  {
//--- scope the terminal's history cache to the requested range
   if(!::HistorySelect(from, to))
     {
      ::Print("SessionDashboard: HistorySelect failed, error ", ::GetLastError());
      return(0);
     }
//--- resize the output array to the maximum possible size
   int total = ::HistoryDealsTotal();
   ::ArrayResize(records, total);
   int      populated       = 0;
   ulong    ticket          = 0;
   long     entry_type      = 0;
   double   profit          = 0.0;
   string   symbol          = "";
   datetime close_time_utc  = 0;
   datetime open_time       = 0;
   ulong    position_id     = 0;
//--- iterate every deal in the selected history range
   for(int i = 0; i < total; i++)
     {
      ticket = ::HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;
      //--- read the entry type and skip anything that is not a closing deal
      entry_type = ::HistoryDealGetInteger(ticket, DEAL_ENTRY);
      if(entry_type != DEAL_ENTRY_OUT && entry_type != DEAL_ENTRY_INOUT)
         continue;
      //--- read the fields needed for this deal record
      profit          = ::HistoryDealGetDouble(ticket, DEAL_PROFIT);
      symbol          = ::HistoryDealGetString(ticket, DEAL_SYMBOL);
      close_time_utc  = (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME);
      position_id     = (ulong)::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      //--- recover the open time from the earliest deal on this position
      open_time       = FindOpenTime(position_id, close_time_utc);
      //--- populate the record and advance the output count
      records[populated].ticket       = ticket;
      records[populated].open_time    = open_time;
      records[populated].close_time   = close_time_utc;
      records[populated].profit       = profit;
      records[populated].symbol       = symbol;
      records[populated].session_type = SESSION_UNKNOWN;
      populated++;
     }
//--- shrink the array down to the number of records actually populated
   ::ArrayResize(records, populated);
   return(populated);
  }

FindOpenTime() recovers a position's opening time by scanning for the earliest deal sharing the same DEAL_POSITION_ID. When that earlier deal cannot be found, the search falls back to the close time itself, giving a hold time of zero for that record.

//+------------------------------------------------------------------+
//| FindOpenTime                                                     |
//+------------------------------------------------------------------+
datetime CDealHistoryReader::FindOpenTime(ulong position_id, datetime close_time_utc) const
  {
   int      total        = ::HistoryDealsTotal();
   datetime earliest     = close_time_utc;
   bool     found        = false;
   ulong    ticket       = 0;
   ulong    scanned_id   = 0;
   datetime scanned_time = 0;
//--- scan every deal for one that shares the target position id
   for(int i = 0; i < total; i++)
     {
      ticket     = ::HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;
      scanned_id = (ulong)::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      if(scanned_id != position_id)
         continue;
      scanned_time = (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME);
      //--- keep the earliest matching time seen so far
      if(!found || scanned_time < earliest)
        {
         earliest = scanned_time;
         found    = true;
        }
     }
   return(earliest);
  }



Section 3 — CSessionClassifier: Classifying Deals by Session

The classifier's job is to look at a deal's UTC close time and decide which of the four sessions it belongs to. Close time is the natural anchor here rather than open time, since a session breakdown is meant to show which market conditions a trade was actually resolved in. A position can be opened during one session and closed during another; classifying by the close time credits the session whose conditions actually produced the outcome, which is the session a trader is trying to evaluate.

//+------------------------------------------------------------------+
//|                                             SessionClassifier.mqh|
//+------------------------------------------------------------------+
#ifndef SESSIONCLASSIFIER_MQH
#define SESSIONCLASSIFIER_MQH

#include "SessionEnums.mqh"

#define SESSION_BOUNDARY_COUNT 4

//+------------------------------------------------------------------+
//| CSessionClassifier                                               |
//| Classifies a UTC datetime into one of the four configured        |
//| trading sessions.                                                |
//+------------------------------------------------------------------+
class CSessionClassifier
  {
private:
   CSessionBoundary      m_boundaries[SESSION_BOUNDARY_COUNT];
   int                   m_boundary_count;

public:
                     CSessionClassifier(void);
                    ~CSessionClassifier(void);

   void                  LoadDefaults(void);
   ENUM_TRADING_SESSION  Classify(datetime close_time_utc) const;
   int                   BoundaryCount(void) const;
   bool                  GetBoundary(int index, CSessionBoundary &boundary_out) const;
  };

//+------------------------------------------------------------------+
//| Constructor: initializes the boundary count to zero until        |
//| LoadDefaults or a custom loader populates the array.             |
//+------------------------------------------------------------------+
CSessionClassifier::CSessionClassifier(void)
  {
   m_boundary_count = 0;
  }

//+------------------------------------------------------------------+
//| Destructor: no dynamic resources to release.                     |
//+------------------------------------------------------------------+
CSessionClassifier::~CSessionClassifier(void)
  {
  }

LoadDefaults() populates the four standard sessions with their commonly accepted UTC boundaries. London and New York overlap between 13:00 and 17:00 UTC, since both centers are genuinely open at once. London is inserted before New York on purpose, because Classify() resolves overlaps by returning the first matching boundary.

//+------------------------------------------------------------------+
//| LoadDefaults                                                     |
//+------------------------------------------------------------------+
void CSessionClassifier::LoadDefaults(void)
  {
//--- define the Sydney session, which wraps past midnight in UTC
   m_boundaries[0].session_type   = SESSION_SYDNEY;
   m_boundaries[0].name           = "Sydney";
   m_boundaries[0].utc_open_hour  = 22;
   m_boundaries[0].utc_close_hour = 7;
   m_boundaries[0].bar_color      = clrForestGreen;
//--- define the Tokyo session
   m_boundaries[1].session_type   = SESSION_TOKYO;
   m_boundaries[1].name           = "Tokyo";
   m_boundaries[1].utc_open_hour  = 0;
   m_boundaries[1].utc_close_hour = 9;
   m_boundaries[1].bar_color      = clrForestGreen;
//--- define the London session, inserted before New York deliberately
   m_boundaries[2].session_type   = SESSION_LONDON;
   m_boundaries[2].name           = "London";
   m_boundaries[2].utc_open_hour  = 8;
   m_boundaries[2].utc_close_hour = 17;
   m_boundaries[2].bar_color      = clrForestGreen;
//--- define the New York session
   m_boundaries[3].session_type   = SESSION_NEW_YORK;
   m_boundaries[3].name           = "New York";
   m_boundaries[3].utc_open_hour  = 13;
   m_boundaries[3].utc_close_hour = 22;
   m_boundaries[3].bar_color      = clrForestGreen;
//--- record how many boundaries were loaded
   m_boundary_count = SESSION_BOUNDARY_COUNT;
  }

Classify() extracts the UTC hour with TimeToStruct() and checks it against each boundary in order. A single branch handles both same-day windows and the midnight-crossing Sydney window, so no boundary needs special-casing by name.

//+------------------------------------------------------------------+
//| Classify                                                         |
//+------------------------------------------------------------------+
ENUM_TRADING_SESSION CSessionClassifier::Classify(datetime close_time_utc) const
  {
//--- extract the UTC hour component from the close time
   MqlDateTime dt;
   ::TimeToStruct(close_time_utc, dt);
   int hour = dt.hour;
//--- compare the hour against each configured boundary in order
   for(int i = 0; i < m_boundary_count; i++)
     {
      int open_hour  = m_boundaries[i].utc_open_hour;
      int close_hour = m_boundaries[i].utc_close_hour;
      //--- same-day boundary: open hour precedes close hour
      if(open_hour < close_hour)
        {
         if(hour >= open_hour && hour < close_hour)
            return(m_boundaries[i].session_type);
        }
      //--- midnight-crossing boundary: open hour follows close hour
      else
        {
         if(hour >= open_hour || hour < close_hour)
            return(m_boundaries[i].session_type);
        }
     }
//--- no configured boundary matched this hour
   return(SESSION_UNKNOWN);
  }

BoundaryCount() and GetBoundary() let a caller, such as the verification script, inspect the loaded boundaries without duplicating them elsewhere.

//+------------------------------------------------------------------+
//| BoundaryCount                                                    |
//| Returns the number of boundaries currently loaded.               |
//+------------------------------------------------------------------+
int CSessionClassifier::BoundaryCount(void) const
  {
   return(m_boundary_count);
  }

//+------------------------------------------------------------------+
//| GetBoundary                                                      |
//| Copies the boundary at the given index into boundary_out.        |
//| Returns false if the index is out of range.                      |
//+------------------------------------------------------------------+
bool CSessionClassifier::GetBoundary(int index, CSessionBoundary &boundary_out) const
  {
//--- validates the requested index before copying
   if(index < 0 || index >= m_boundary_count)
      return(false);
   boundary_out = m_boundaries[index];
   return(true);
  }

This classification assumes a deal's close time is already reported in UTC, the convention MQL5's own history functions follow; Section 10 covers what to check if that assumption does not hold for a given broker.



Section 4 — CSessionAggregator: Computing Session Metrics and Account Totals

The aggregator collapses the flat deal array into one CSessionMetrics entry per trading session, and can also fold those entries into a single set of account-wide totals.

//+------------------------------------------------------------------+
//|                                            SessionAggregator.mqh |
//+------------------------------------------------------------------+
#ifndef SESSIONAGGREGATOR_MQH
#define SESSIONAGGREGATOR_MQH

#include "SessionEnums.mqh"

#define SESSION_TYPE_COUNT 5

//+------------------------------------------------------------------+
//| CSessionAggregator                                               |
//| Groups deal records by session type and computes net P&L, win    |
//| rate inputs, trade count, and average hold time for each.        |
//+------------------------------------------------------------------+
class CSessionAggregator
  {
private:
   string                SessionName(ENUM_TRADING_SESSION session_type) const;

public:
                     CSessionAggregator(void);
                    ~CSessionAggregator(void);

   int                   Aggregate(const CDealRecord &records[], int count,
                                   CSessionMetrics &metrics[]);
   void                  ComputeTotals(const CSessionMetrics &metrics[], int count,
                                       int &total_trades_out, double &total_win_rate_out,
                                       double &total_net_pnl_out);
  };

//+------------------------------------------------------------------+
//| Constructor: no member state to initialize.                      |
//+------------------------------------------------------------------+
CSessionAggregator::CSessionAggregator(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor: no dynamic resources to release.                     |
//+------------------------------------------------------------------+
CSessionAggregator::~CSessionAggregator(void)
  {
  }

Aggregate() scans the full deal array once for each of the five possible session types, accumulating trade count, win count, net P&L, and total hold time. With only five session types and a deal count that rarely exceeds a few thousand rows, this repeated scan is fast enough that a more complex grouping structure would not be worth the added complexity.

//+------------------------------------------------------------------+
//| Aggregate                                                        |
//+------------------------------------------------------------------+
int CSessionAggregator::Aggregate(const CDealRecord &records[], int count,
                                  CSessionMetrics &metrics[])
  {
//--- size the metrics array to hold every possible session type
   ::ArrayResize(metrics, SESSION_TYPE_COUNT);
   int                   trade_count     = 0;
   int                   win_count       = 0;
   double                net_pnl         = 0.0;
   long                  total_hold_time = 0;
   long                  hold_seconds    = 0;
   ENUM_TRADING_SESSION  current_type    = SESSION_SYDNEY;
//--- compute metrics for each of the five possible session types
   for(int s = 0; s < SESSION_TYPE_COUNT; s++)
     {
      current_type    = (ENUM_TRADING_SESSION)s;
      trade_count     = 0;
      win_count       = 0;
      net_pnl         = 0.0;
      total_hold_time = 0;
      //--- scan every deal record for matches to the current session type
      for(int i = 0; i < count; i++)
        {
         if(records[i].session_type != current_type)
            continue;
         trade_count++;
         net_pnl += records[i].profit;
         if(records[i].profit > 0.0)
            win_count++;
         hold_seconds     = (long)(records[i].close_time - records[i].open_time);
         total_hold_time += hold_seconds;
        }
      //--- populate the metrics entry for this session type
      metrics[s].session_type     = current_type;
      metrics[s].name             = SessionName(current_type);
      metrics[s].trade_count      = trade_count;
      metrics[s].win_count        = win_count;
      metrics[s].net_pnl          = net_pnl;
      metrics[s].total_hold_time  = total_hold_time;
      //--- guard against division by zero for sessions with no trades
      if(trade_count > 0)
         metrics[s].average_hold_time = (double)total_hold_time / (double)trade_count;
      else
         metrics[s].average_hold_time = 0.0;
     }
   return(SESSION_TYPE_COUNT);
  }

ComputeTotals() sums trade count, win count, and net P&L across every entry in the metrics array, including SESSION_UNKNOWN, so the total always matches the number of records the reader pulled from history. This is what lets the bar chart's summary row report a single account-wide trade count, win rate, and net P&L alongside the four individual session bars.

//+------------------------------------------------------------------+
//| ComputeTotals                                                    |
//+------------------------------------------------------------------+
void CSessionAggregator::ComputeTotals(const CSessionMetrics &metrics[], int count,
                                       int &total_trades_out, double &total_win_rate_out,
                                       double &total_net_pnl_out)
  {
   int    total_trades  = 0;
   int    total_wins    = 0;
   double total_pnl     = 0.0;
//--- sum trade count, win count, and net P&L across every session
   for(int i = 0; i < count; i++)
     {
      total_trades += metrics[i].trade_count;
      total_wins   += metrics[i].win_count;
      total_pnl    += metrics[i].net_pnl;
     }
   total_trades_out = total_trades;
//--- guard against division by zero when no trades exist at all
   if(total_trades > 0)
      total_win_rate_out = ((double)total_wins / (double)total_trades) * 100.0;
   else
      total_win_rate_out = 0.0;
   total_net_pnl_out = total_pnl;
  }

SessionName() is a small private helper that maps the enum value to the display string used by both the bar chart and the table printer.

//+------------------------------------------------------------------+
//| SessionName                                                      |
//| Returns the display name for a given session type.               |
//+------------------------------------------------------------------+
string CSessionAggregator::SessionName(ENUM_TRADING_SESSION session_type) const
  {
   switch(session_type)
     {
      case SESSION_SYDNEY:
         return("Sydney");
      case SESSION_TOKYO:
         return("Tokyo");
      case SESSION_LONDON:
         return("London");
      case SESSION_NEW_YORK:
         return("New York");
      default:
         return("Unknown");
     }
  }



Section 5 — CSessionBarChart: Rendering with CCanvas

The canvas panel is created with CCanvas::CreateBitmapLabel(), which attaches a single bitmap object to the chart that the class has full pixel-level control over.

//+------------------------------------------------------------------+
//|                                              SessionBarChart.mqh |
//+------------------------------------------------------------------+
#ifndef SESSIONBARCHART_MQH
#define SESSIONBARCHART_MQH

#include <Canvas\Canvas.mqh>
#include "SessionEnums.mqh"

//+------------------------------------------------------------------+
//| CSessionBarChart                                                 |
//| Renders session metrics as a horizontal, color-coded bar chart   |
//| on the chart using a CCanvas panel.                              |
//+------------------------------------------------------------------+
class CSessionBarChart
  {
private:
   CCanvas               m_canvas;
   string                m_object_name;
   bool                  m_created;
   string                m_font_name;
   int                   m_font_size;

public:
                     CSessionBarChart(void);
                    ~CSessionBarChart(void);

   bool                  Draw(const CSessionMetrics &metrics[], int count,
                              int total_trades, double total_win_rate, double total_net_pnl,
                              int x, int y, int width, int height);
   void                  Clear(void);
  };

//+------------------------------------------------------------------+
//| Constructor: assigns a unique object name for the canvas panel,  |
//| fixes the font used for both drawing and measuring text, and     |
//| marks the panel as not yet created.                              |
//+------------------------------------------------------------------+
CSessionBarChart::CSessionBarChart(void)
  {
   m_object_name = "SessionBarChartPanel";
   m_created     = false;
   m_font_name   = "Arial";
   m_font_size   = 16;
  }

//+------------------------------------------------------------------+
//| Destructor: intentionally does not remove the canvas panel. The  |
//| panel is a chart object owned by the chart itself, and must      |
//| remain visible after the CSessionBarChart instance that drew it  |
//| goes out of scope at the end of a script's OnStart.              |
//+------------------------------------------------------------------+
CSessionBarChart::~CSessionBarChart(void)
  {
  }

Draw() now takes three extra parameters: the account-wide total trade count, win rate, and net P&L, alongside the per-session metrics it already drew. It reserves one extra row at the bottom of the panel for these totals, dividing the panel height by count + 1 instead of count so the summary row gets the same vertical space as a session bar.

//+------------------------------------------------------------------+
//| Draw                                                             |
//+------------------------------------------------------------------+
bool CSessionBarChart::Draw(const CSessionMetrics &metrics[], int count,
                            int total_trades, double total_win_rate, double total_net_pnl,
                            int x, int y, int width, int height)
  {
//--- removes any previously drawn panel before creating a new one
   Clear();
//--- creates the bitmap label that the canvas will draw onto
   if(!m_canvas.CreateBitmapLabel(m_object_name, x, y, width, height, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::Print("SessionDashboard: CreateBitmapLabel failed, error ", ::GetLastError());
      return(false);
     }
   m_created = true;
//--- fix a known font for both drawing and measurement, so the two never disagree
   m_canvas.FontSet(m_font_name, m_font_size);
   ::TextSetFont(m_font_name, m_font_size);
//--- clears the panel to a light background before drawing
   m_canvas.Erase(::ColorToARGB(clrWhiteSmoke, 255));
//--- finds the largest absolute net P&L among sessions with trades
   double max_abs_pnl = 0.0;
   for(int i = 0; i < count; i++)
     {
      if(metrics[i].trade_count == 0)
         continue;
      double abs_pnl = ::MathAbs(metrics[i].net_pnl);
      if(abs_pnl > max_abs_pnl)
         max_abs_pnl = abs_pnl;
     }
   if(max_abs_pnl <= 0.0)
      max_abs_pnl = 1.0;
//--- reserves one extra row at the bottom of the panel for the summary line
   int zero_x     = width / 2;
   int half_width = (width / 2) - 20;
   int row_height = height / (count + 1);
//--- draws the vertical zero line spanning only the session bar rows
   m_canvas.LineVertical(zero_x, 0, row_height * count, ::ColorToARGB(clrDarkGray, 255));
//--- draws one bar per session
   int      bar_y       = 0;
   int      bar_x       = 0;
   int      bar_width   = 0;
   uint     bar_color   = 0;
   string   pnl_text    = "";
   for(int i = 0; i < count; i++)
     {
      bar_y = i * row_height + (row_height / 4);
      //--- Uses green for profitable sessions and red for the rest
      bar_color = (metrics[i].net_pnl > 0.0) ? ::ColorToARGB(clrForestGreen, 255)
                  : ::ColorToARGB(clrCrimson, 255);
      //--- computes bar width proportional to this session's net P&L
      bar_width = (int)((::MathAbs(metrics[i].net_pnl) / max_abs_pnl) * half_width);
      //--- draws the bar extending right for profit, left for loss
      if(metrics[i].net_pnl > 0.0)
         bar_x = zero_x;
      else
         bar_x = zero_x - bar_width;
      m_canvas.FillRectangle(bar_x, bar_y, bar_x + bar_width, bar_y + (row_height / 2), bar_color);
      //--- draws the session name label on the left side of the panel
      m_canvas.TextOut(10, bar_y, metrics[i].name, ::ColorToARGB(clrBlack, 255));
      //--- draws the net P&L value on the right side of the panel
      pnl_text = ::DoubleToString(metrics[i].net_pnl, 2);
      m_canvas.TextOut(width - 90, bar_y, pnl_text, ::ColorToARGB(clrBlack, 255));
     }
//--- draw a divider line separating the bars from the summary row
   int summary_y = row_height * count;
   m_canvas.LineHorizontal(0, summary_y, width, ::ColorToARGB(clrDarkGray, 255));
//--- draws the three account-wide totals in sequence, each positioned after
//--- the ACTUAL measured width of the previous one plus a fixed gap, so a
//--- longer string can never overlap the one that follows it
   int    text_y       = summary_y + (row_height / 4);
   int    gap          = 16;
   uint    measured_w  = 0;
   uint    measured_h  = 0;
   string trades_text  = "Trades: " + ::IntegerToString(total_trades);
   string winrate_text = "Win rate: " + ::DoubleToString(total_win_rate, 1) + "%";
   string netpnl_text  = "Net P&L: " + ::DoubleToString(total_net_pnl, 2);
   int    trades_x     = 10;
   ::TextGetSize(trades_text, measured_w, measured_h);
   int    winrate_x    = trades_x + (int)measured_w + gap;
   ::TextGetSize(winrate_text, measured_w, measured_h);
   int    netpnl_x     = winrate_x + (int)measured_w + gap;
   m_canvas.TextOut(trades_x,  text_y, trades_text,  ::ColorToARGB(clrBlack, 255));
   m_canvas.TextOut(winrate_x, text_y, winrate_text, ::ColorToARGB(clrBlack, 255));
   m_canvas.TextOut(netpnl_x,  text_y, netpnl_text,  ::ColorToARGB(clrBlack, 255));
//--- commit the drawing to the chart
   m_canvas.Update();
   return(true);
  }

Immediately after creating the panel, Draw() calls CCanvas::FontSet() and the global ::TextSetFont() with the same name and size. FontSet() fixes what the canvas itself draws with; ::TextSetFont() fixes what the global ::TextGetSize() function measures against. Calling both with identical arguments means a measurement taken later is guaranteed to match what actually gets drawn.

The bar-drawing loop itself is unchanged from computing each session's proportional bar width against the largest absolute net P&L, and coloring it green or red by the sign of net_pnl.

Clear() removes the bitmap and resets the created flag. It still runs at the top of Draw() so a redraw does not leave a stale bitmap behind, and it remains available for any caller who wants to tear the panel down explicitly.

//+------------------------------------------------------------------+
//| Clear                                                            |
//| Removes the canvas panel from the chart if it was created.       |
//+------------------------------------------------------------------+
void CSessionBarChart::Clear(void)
  {
   if(!m_created)
      return;
   m_canvas.Destroy();
   m_created = false;
  }



Section 6 — CSessionTablePrinter: Experts tab Summary

A plain-text table serves a different purpose than the canvas panel: it can be copied into a report or compared line by line against a previous run without opening the chart.

//+------------------------------------------------------------------+
//|                                          SessionTablePrinter.mqh |
//+------------------------------------------------------------------+
#ifndef SESSIONTABLEPRINTER_MQH
#define SESSIONTABLEPRINTER_MQH

#include "SessionEnums.mqh"

//+------------------------------------------------------------------+
//| CSessionTablePrinter                                             |
//| Prints a formatted summary table of session metrics to the       |
//| Experts tab with fixed-width columns.                            |
//+------------------------------------------------------------------+
class CSessionTablePrinter
  {
private:
   string                PadRight(string value, int width) const;

public:
                     CSessionTablePrinter(void);
                    ~CSessionTablePrinter(void);

   void                  Print(const CSessionMetrics &metrics[], int count);
  };

//+------------------------------------------------------------------+
//| Constructor: no member state to initialize.                      |
//+------------------------------------------------------------------+
CSessionTablePrinter::CSessionTablePrinter(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor: no dynamic resources to release.                     |
//+------------------------------------------------------------------+
CSessionTablePrinter::~CSessionTablePrinter(void)
  {
  }

Print() writes the header row and then one row per session, in the order a trader would naturally scan: name, trade count, win rate, net P&L, and average hold time.

//+------------------------------------------------------------------+
//| Print                                                            |
//| Outputs a formatted table to the terminal log with columns for   |
//| session name, trade count, win rate, net P&L, and average hold   |
//| time, one row per session.                                       |
//+------------------------------------------------------------------+
void CSessionTablePrinter::Print(const CSessionMetrics &metrics[], int count)
  {
   string   header      = "";
   string   row         = "";
   double   win_rate    = 0.0;
//--- print the header row with fixed-width column labels
   header = PadRight("Session", 12) + PadRight("Trades", 10) +
            PadRight("Win Rate", 12) + PadRight("Net P&L", 14) +
            PadRight("Avg Hold(s)", 14);
   ::Print(header);
//--- print one row per session with the same fixed-width columns
   for(int i = 0; i < count; i++)
     {
      //--- guard against division by zero for sessions with no trades
      if(metrics[i].trade_count > 0)
         win_rate = ((double)metrics[i].win_count / (double)metrics[i].trade_count) * 100.0;
      else
         win_rate = 0.0;
      row = PadRight(metrics[i].name, 12) +
            PadRight(::IntegerToString(metrics[i].trade_count), 10) +
            PadRight(::DoubleToString(win_rate, 1) + "%", 12) +
            PadRight(::DoubleToString(metrics[i].net_pnl, 2), 14) +
            PadRight(::DoubleToString(metrics[i].average_hold_time, 1), 14);
      ::Print(row);
     }
  }

PadRight() is the shared column-alignment helper the header and every row lean on, padding with trailing spaces or leaving a value unchanged if it already fills the column.

//+------------------------------------------------------------------+
//| PadRight                                                         |
//+------------------------------------------------------------------+
string CSessionTablePrinter::PadRight(string value, int width) const
  {
   int need = width - ::StringLen(value);
//--- return the value unchanged if it already fills the column
   if(need <= 0)
      return(value);
   string result = value;
   for(int i = 0; i < need; i++)
      result += " ";
   return(result);
  }



Section 7 — SessionDashboard.mq5: Assembling the Main Script

This script serves as a coordinator. It contains no core business logic of its own; instead, it instantiates and connects the five previously defined classes to execute the data pipeline in its natural sequence.

Its only standalone task is to convert the user's inputs into a concrete date range. For simpler configuration, the script accepts a lookback-days parameter instead of exact datetime inputs. It then anchors this lookback period against TimeCurrent() to dynamically calculate the precise from and to timestamps required by the history reader.

//+------------------------------------------------------------------+
//|                                             SessionDashboard.mq5 |
//+------------------------------------------------------------------+
#property script_show_inputs

#include <SessionDashboard/SessionEnums.mqh>
#include <SessionDashboard/SessionClassifier.mqh>
#include <SessionDashboard/DealHistoryReader.mqh>
#include <SessionDashboard/SessionAggregator.mqh>
#include <SessionDashboard/SessionBarChart.mqh>
#include <SessionDashboard/SessionTablePrinter.mqh>

input int InpLookbackDays = 30;    // Number of days to look back from the current time
input int InpPanelX       = 20;    // Canvas panel X coordinate
input int InpPanelY       = 20;    // Canvas panel Y coordinate
input int InpPanelWidth   = 420;   // Canvas panel width in pixels
input int InpPanelHeight  = 260;   // Canvas panel height in pixels

OnStart() resolves the lookback into a concrete date range, reads, classifies, and aggregates, then calls ComputeTotals() right after aggregation to fold the per-session metrics into a single set of account-wide numbers. Those totals are passed straight into Draw() alongside the per-session metrics, so the canvas panel's summary row always reflects the same data as the four bars above it.

//+------------------------------------------------------------------+
//| OnStart                                                          |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- resolve the date range from the lookback input
   datetime to_time   = ::TimeCurrent();
   datetime from_time = to_time - (InpLookbackDays * 86400);
//--- read closed deal history for the resolved date range
   CDealHistoryReader reader;
   CDealRecord        records[];
   int                record_count = reader.Read(from_time, to_time, records);
   ::Print("SessionDashboard: read ", record_count, " deal records");
   if(record_count == 0)
     {
      ::Print("SessionDashboard: no closed deals found in the selected range");
      return;
     }
//--- classify every deal record into a trading session
   CSessionClassifier classifier;
   classifier.LoadDefaults();
   for(int i = 0; i < record_count; i++)
      records[i].session_type = classifier.Classify(records[i].close_time);
//--- aggregate the classified records into per-session metrics
   CSessionAggregator aggregator;
   CSessionMetrics    metrics[];
   int                metrics_count = aggregator.Aggregate(records, record_count, metrics);
   ::Print("SessionDashboard: aggregated ", metrics_count, " session metric rows");
//--- fold the per-session metrics into account-wide totals
   int    total_trades   = 0;
   double total_win_rate = 0.0;
   double total_net_pnl  = 0.0;
   aggregator.ComputeTotals(metrics, metrics_count, total_trades, total_win_rate, total_net_pnl);
//--- render the canvas panel bar chart, including the summary row
   CSessionBarChart bar_chart;
   bar_chart.Draw(metrics, metrics_count, total_trades, total_win_rate, total_net_pnl,
                  InpPanelX, InpPanelY, InpPanelWidth, InpPanelHeight);
//--- print the summary table to the Experts tab
   CSessionTablePrinter printer;
   printer.Print(metrics, metrics_count);
  }



Section 8 — TestSessionAnalytics.mq5: Verification Script

The verification script serves as a unit test suite targeting the logic most prone to edge-case bugs: exact boundary hour classification, the midnight-spanning Sydney session, P&L aggregation, and average hold time calculations. Each test constructs a minimal input array to isolate specific class behaviors. The outputs are then validated using a custom ASSERT macro, forcing explicit pass/fail logs in the Experts tab to prevent silent execution failures.

//+------------------------------------------------------------------+
//|                                         TestSessionAnalytics.mq5 |
//+------------------------------------------------------------------+

#include <SessionDashboard/SessionEnums.mqh>
#include <SessionDashboard/SessionClassifier.mqh>
#include <SessionDashboard/SessionAggregator.mqh>

#define ASSERT(condition, message) TestAssert((condition), (message))

int g_pass_count = 0;
int g_fail_count = 0;

//+------------------------------------------------------------------+
//| TestAssert                                                       |
//| Prints a pass or fail message for a single test condition and    |
//| tracks the running pass and fail counts.                         |
//+------------------------------------------------------------------+
void TestAssert(bool condition, string message)
  {
   if(condition)
     {
      g_pass_count++;
      ::Print("PASS: ", message);
     }
   else
     {
      g_fail_count++;
      ::Print("FAIL: ", message);
     }
  }

BuildDateTime() constructs a fixed test date at a given hour, so every test operates on the same calendar day and only the hour varies.

//+------------------------------------------------------------------+
//| BuildDateTime                                                    |
//+------------------------------------------------------------------+
datetime BuildDateTime(int hour)
  {
   MqlDateTime dt;
   dt.year = 2026;
   dt.mon  = 1;
   dt.day  = 15;
   dt.hour = hour;
   dt.min  = 30;
   dt.sec  = 0;
   return(::StructToTime(dt));
  }

OnStart() runs eight assertions: the four covering session classification, the win rate and net P&L formulas from the same six-trade fixture, the average hold time formula, and a final group verifying that ComputeTotals() correctly folds that same six-trade fixture into an account-wide total.

//+------------------------------------------------------------------+
//| OnStart                                                          |
//| Runs classification, aggregation, and hold time tests and        |
//| prints a final summary of pass and fail counts.                  |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   CSessionClassifier classifier;
   classifier.LoadDefaults();
//--- test 1: 14:30 UTC falls in the London/New York overlap, expect London
   ENUM_TRADING_SESSION overlap_result = classifier.Classify(BuildDateTime(14));
   ASSERT(overlap_result == SESSION_LONDON,
          "14:30 UTC overlap classifies as SESSION_LONDON");
//--- test 2: 00:30 UTC falls inside the midnight-crossing Sydney boundary
   ENUM_TRADING_SESSION sydney_result = classifier.Classify(BuildDateTime(0));
   ASSERT(sydney_result == SESSION_SYDNEY,
          "00:30 UTC classifies as SESSION_SYDNEY across midnight");
//--- test 3: 03:00 UTC also falls inside the Sydney boundary
   ENUM_TRADING_SESSION sydney_result_2 = classifier.Classify(BuildDateTime(3));
   ASSERT(sydney_result_2 == SESSION_SYDNEY,
          "03:00 UTC classifies as SESSION_SYDNEY across midnight");
//--- test 4: 10:00 UTC falls inside the London-only window, before overlap
   ENUM_TRADING_SESSION london_only = classifier.Classify(BuildDateTime(10));
   ASSERT(london_only == SESSION_LONDON,
          "10:00 UTC classifies as SESSION_LONDON before the overlap");
//--- build a set of deal records to exercise the aggregator
   CDealRecord records[6];
   for(int i = 0; i < 6; i++)
      records[i].session_type = SESSION_LONDON;
   records[0].profit      = 100.0;
   records[0].open_time   = BuildDateTime(8);
   records[0].close_time  = BuildDateTime(8) + 1800;
   records[1].profit      = 50.0;
   records[1].open_time   = BuildDateTime(8);
   records[1].close_time  = BuildDateTime(8) + 3600;
   records[2].profit      = 75.0;
   records[2].open_time   = BuildDateTime(8);
   records[2].close_time  = BuildDateTime(8) + 5400;
   records[3].profit      = -20.0;
   records[3].open_time   = BuildDateTime(8);
   records[3].close_time  = BuildDateTime(8);
   records[4].profit      = -30.0;
   records[4].open_time   = BuildDateTime(8);
   records[4].close_time  = BuildDateTime(8);
   records[5].profit      = 25.0;
   records[5].open_time   = BuildDateTime(8);
   records[5].close_time  = BuildDateTime(8);
//--- test 5: win rate for 4 wins out of 6 deals should be 66.7%
   CSessionAggregator aggregator;
   CSessionMetrics    metrics[];
   int                metrics_count = aggregator.Aggregate(records, 6, metrics);
   double win_rate = ((double)metrics[SESSION_LONDON].win_count /
                      (double)metrics[SESSION_LONDON].trade_count) * 100.0;
   ASSERT(::MathAbs(win_rate - 66.7) < 0.05,
          "win rate for 4 of 6 winning deals rounds to 66.7%");
//--- test 6: net P&L for the same six deals should sum to 200.0
   ASSERT(::MathAbs(metrics[SESSION_LONDON].net_pnl - 200.0) < 0.001,
          "net P&L across the six test deals sums to 200.0");
//--- test 7: average hold time for three deals of 1800, 3600, 5400 seconds
   CDealRecord hold_records[3];
   hold_records[0].session_type = SESSION_TOKYO;
   hold_records[0].profit       = 10.0;
   hold_records[0].open_time    = BuildDateTime(1);
   hold_records[0].close_time   = BuildDateTime(1) + 1800;
   hold_records[1].session_type = SESSION_TOKYO;
   hold_records[1].profit       = 10.0;
   hold_records[1].open_time    = BuildDateTime(1);
   hold_records[1].close_time   = BuildDateTime(1) + 3600;
   hold_records[2].session_type = SESSION_TOKYO;
   hold_records[2].profit       = 10.0;
   hold_records[2].open_time    = BuildDateTime(1);
   hold_records[2].close_time   = BuildDateTime(1) + 5400;
   CSessionMetrics hold_metrics[];
   aggregator.Aggregate(hold_records, 3, hold_metrics);
   ASSERT(::MathAbs(hold_metrics[SESSION_TOKYO].average_hold_time - 3600.0) < 0.001,
          "average hold time for 1800, 3600, 5400 seconds computes to 3600.0");
//--- print the final summary of pass and fail counts
   ::Print("TestSessionAnalytics: ", g_pass_count, " passed, ", g_fail_count, " failed");
  }


Session dashboard mock-up image

Dashboard mockup: four session bars extend left (red, loss) or right (green, profit) from the center zero line, with a divider and summary row below showing the account-wide trade count, win rate, and net P&L, exactly as CSessionBarChart::Draw() renders it.



Section 9 — Extending the Dashboard

Once the core dashboard is functional, the foundational classes can be easily extended to support more advanced analysis. For instance, deals executed outside the four major boundaries currently default to SESSION_UNKNOWN. To actively track these quiet overnight periods, you can introduce a dedicated "Off-Hours" session. This requires extending ENUM_TRADING_SESSION with a new value, updating the CSessionAggregator count, and adding a final catch-all boundary in CSessionClassifier::Classify() to capture any remaining hours rather than letting them fall through.

Another valuable extension is isolating performance by symbol to determine which specific instruments drive a session's profitability. The simplest implementation adds a symbol filter directly inside CDealHistoryReader::Read(), allowing the user to pass in _Symbol to ignore non-matching deals and view one instrument in isolation. For a more comprehensive breakdown across the entire account, you could introduce a CSymbolSessionMetrics struct and add a new aggregation method to group data by both session and symbol, reusing the existing scanning logic.

To facilitate external analysis or advanced charting, exporting the computed metrics to a CSV file is a natural companion to the Experts tab table. This can be achieved by using FileOpen() with the FILE_WRITE and FILE_CSV flags. A straightforward loop would then iterate through the session rows, using FileWrite() to output the exact same five columns generated by the CSessionTablePrinter before securely closing the handle with FileClose().

Finally, adding a secondary date input would allow traders to compare historical periods side by side, such as the current month versus the previous month. Implementing this involves running the read, classify, and aggregate pipeline twice—once for each date range. The CSessionBarChart::Draw() method could then be updated to accept a second metrics array, rendering paired bars on the canvas. Using lighter shades of the existing red and green ensures the two periods are easily distinguishable without cluttering the interface with unrelated colors.



Section 10: Limitations

The dashboard uses fixed UTC session boundaries and does not account for brokers whose server clocks shift for daylight saving time. The classifier uses the UTC hour extracted from the deal's close time and assumes that timestamp is already in UTC. Under that assumption, broker daylight-saving shifts do not affect results. If your broker reports deal times differently, verify the time basis before using the classification.

If the open time cannot be recovered (for example, when the position was opened before the selected date range), the code falls back to using the close time as the open time. This produces a zero hold time for that record. This understates the average hold time for any session containing such deals, and a trader working with data near the edge of a lookback window should be aware that widening the lookback range further back in time will typically produce more accurate hold time figures for the earliest deals in the original range.

Because sessions overlap, a deal closed at 14:00 UTC can belong to either London or New York. The classifier resolves this by assigning the deal to whichever session appears first in the boundary array, which in the default configuration is London. This is a simplification chosen for predictability rather than an attempt to model which market was truly more responsible for the deal's outcome, and a trader who wants the opposite resolution can simply reorder the boundary array in LoadDefaults().

CDealHistoryReader::Read() applies no filter based on symbol, so every closed deal in the queried date range is included in the metrics, regardless of which instrument it was traded on. A trader running the script from different chart symbols will see identical output every time, since the underlying query never compares DEAL_SYMBOL against the chart's own symbol. This is by design for this version of the dashboard, which is meant to give an account-wide view of session performance, but it means a trader who wants to isolate one instrument's session behavior needs to add that filtering themselves, as described in Section 9.

Finally, the P&L label uses a fixed right-margin offset. It does not measure the rendered text width. A short value like a small win or loss sits comfortably inside the panel, while a longer value with more digits can run close to, or in some cases past, the panel's right border, since the offset does not adjust for how wide the formatted string actually is.



Conclusion

What you get from the article is a complete, runnable pipeline and clear next steps. The distributed components include a history reader, a UTC session classifier, an aggregator that computes net P&L, win rates, trade counts and average hold times, a CCanvas bar chart renderer, and a table printer — plus two scripts you can run immediately in the terminal:

  • SessionDashboard.mq5 — reads history for the input lookback period, classifies and aggregates trades, draws the session bar chart (with an account‑wide summary row), and prints the same data to Experts.
  • TestSessionAnalytics.mq5 — a verification suite that exercises boundary hours, the midnight‑wrapping Sydney session, aggregation sums, and average hold time formulas and reports explicit PASS/FAIL results.

The code is intentionally modular so you can extend it quickly: add an Off‑Hours session, filter by symbol, export CSV, or compare two date ranges by running the pipeline twice and adapting the renderer. Important caveats remain: the classifier uses deal close times in UTC (confirm your broker's time basis), recovered open times fall back to close time if the opening deal is outside the selected range (which underestimates average hold time), and overlapping sessions are resolved by boundary order (default: London before New York). To get started, run TestSessionAnalytics.mq5 to validate the logic for your environment, then run SessionDashboard.mq5 with your chosen lookback and panel settings; tweak session boundaries or add symbol filters as required.


Programs used in the article:

# Name Type Description
1 SessionEnums.mqh Include File Defines ENUM_TRADING_SESSION and the CSessionBoundary, CSessionMetrics, and CDealRecord structs
2 SessionClassifier.mqh Include File CSessionClassifier class: classifies a UTC datetime into a trading session
3 DealHistoryReader.mqh Include File CDealHistoryReader class: reads closed deal records from terminal history across all symbols
4 SessionAggregator.mqh Include File CSessionAggregator class: groups deal records by session, computes metrics, and folds them into account-wide totals
5 SessionBarChart.mqh Include File CSessionBarChart class: renders a persistent CCanvas horizontal bar chart with a summary row
6 SessionTablePrinter.mqh Include File CSessionTablePrinter class: prints the session summary table to the Experts tab
7 SessionDashboard.mq5 Script Main script: wires all components, reads history, classifies deals, aggregates, computes totals, and renders
8 TestSessionAnalytics.mq5 Script Verification script covering classification boundaries, aggregation correctness, hold time, and account-wide totals
9 SessionDashboard.zip Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder. 
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Defining your Edge (Part 6): Harnessing Fourier Transform and a Spiking Neural Network in an Expert Advisor Defining your Edge (Part 6): Harnessing Fourier Transform and a Spiking Neural Network in an Expert Advisor
Article revisits Trading Robot that merged Discrete Fourier Transform with Leaky Integrate-and-Fire Spiking Neural Network. Evaluation is made over the seven operating modes with different symbols, timeframes, and test windows. We use two-thirds of the test window to optimize while the one-third does a forward walk run. This study tries to detail parameter interactions, input scaling, gating effects, and outlines practical checks that include rolling windows, frozen inputs, and others. The goal remains identifying settings worth further testing or paper trading.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
From Basic to Intermediate: Operator Overloading (II) From Basic to Intermediate: Operator Overloading (II)
At first, this article may seem rather confusing because of the material I'm going to cover in it. Nevertheless, I've tried to explain everything as simply and clearly as possible. I hope you'll understand what I'm about to show you here, and that it will come in handy someday.