Session Volume Profile EA MQL5: Code, Settings & Strategy

Build a session volume profile EA in MQL5 that filters opening range breakouts with tick volume confirmation. Full source code, settings, and honest backtest.

session-volume-profile-ea-mql5-code-settings

Most breakout EAs are dumb. They draw a box, wait for a candle to close beyond it, and fire a market order into whatever chaos is happening at that moment. No context, no volume check, no idea whether the breakout is real or just a stop hunt. If you've been trading the London open or New York open for a while, you know the feeling — price pokes above the high, you're in, and then it reverses straight back through the range and stops you out. The volume profile concept fixes a big part of that problem, and in this article I'll show you how to build it into a proper MQL5 EA.

We're going to build a session volume profile EA that does three things. First, it constructs a volume profile for each trading session — Asian, London, and New York — using tick volume data. Second, it identifies the opening range for the active session and marks the high and low. Third, it trades breakouts of that range, but only when volume at the breakout point confirms the move. You'll get the full MQL5 source code, a walkthrough of the logic, and an honest assessment of where this approach works and where it falls apart.

Before we go further, one thing I want to be clear about: this is not a "set and forget" money printer. No EA is. What this code gives you is a disciplined framework that filters out the worst of the false breakouts. You still need to pick the right session, the right symbol, and the right risk settings for your account. I'll point out where I've seen it struggle so you don't have to learn those lessons the expensive way.

Why Session Volume Profile Beats a Plain Opening Range

A standard opening range breakout (ORB) strategy just watches price. If price breaks above the high of the first 30 or 60 minutes, you go long. The problem is that a breakout can happen on thin, erratic volume — especially during the first few minutes of a session when spreads are wide and liquidity is patchy. The session volume profile adds a filter: it shows you where the bulk of trading actually occurred during that session. When price breaks the range at a point where volume is building, you have a much better signal than a breakout on empty air.

Here's the key insight. In a healthy breakout, you'll see the volume profile "paint" new levels beyond the opening range. That means traders are actively transacting at those new prices. In a fake breakout, price spikes beyond the range, but the volume profile stays flat — no one is really trading there. The EA checks this by comparing the volume accumulated in the breakout zone against a threshold. If the threshold isn't met, the trade is skipped.

The session aspect matters because each session has its own character. Asian session volume is typically thinner, and breakouts there are less reliable. London session volume is where the real action starts — the FTSE, DAX, and FX pairs see a massive liquidity injection around 08:00 GMT. New York session brings the overlap with London and the US equity market open. A single volume profile for the whole day would blur these distinct phases together. By building a separate profile for each session, the EA adapts to the personality of each one.

There's also a practical benefit to session-based profiling that most traders overlook: it keeps your analysis anchored to a defined time window. When you look at a daily volume profile, you're mixing overnight noise with the London rush and the New York close. That makes it harder to spot the levels that actually matter for the move you're trying to trade. A session profile isolates the relevant liquidity pools, which is especially useful if you're trading a specific session repeatedly — say, the London open on GBPUSD.

Understanding Tick Volume vs. Real Volume

Before we dive into the code, let's address the elephant in the room. MetaTrader 5 doesn't give you real exchange volume for most instruments. What you get is tick volume — the number of price updates (ticks) that occurred during a bar. For forex, where there's no central exchange, tick volume is actually a reasonable proxy for trading activity. More ticks means more participants hitting the bid or offer, which generally correlates with real volume.

For futures or indices traded via CFDs, the correlation is weaker but still useful. I've backtested this EA on EURUSD, GBPUSD, and XAUUSD, and the tick volume filter consistently reduced false breakouts compared to a naked ORB. It's not perfect — no volume proxy is — but it's a meaningful edge. If you're trading on a platform that provides real volume data (like some futures brokers), the logic ports over directly; you'd just swap iVolume() for the real volume feed.

One nuance worth understanding: tick volume is not uniform across price levels. During fast moves, the tick count per bar can spike simply because the spread widened and price bounced back and forth. That's not necessarily real buying or selling pressure — it's just noise. The EA handles this by looking at the accumulated volume in a zone over a period of time, not just a single bar's tick count. That smooths out some of the spread-related spikes, but it's not a perfect filter. If you're trading a session with notoriously wide spreads — like the Asian session on a low-liquidity pair — you'll want to raise the volume threshold to compensate.

How the Session Volume Profile EA Works

Let's break down the core logic before we get to the code. The EA runs on the M1 timeframe — that's the only sensible choice here because we need granular data for the opening range. The session definitions are configurable, but I'll give you sensible defaults for the three major sessions:

  • Asian session: 00:00 – 08:00 server time
  • London session: 08:00 – 16:00 server time
  • New York session: 13:00 – 21:00 server time

Note that these overlap — London and New York share the 13:00–16:00 window. That's intentional and realistic. The EA uses server time, so you need to know your broker's timezone and adjust the session start/end hours accordingly. A broker on GMT+2 will have London opening at 10:00 server time, not 08:00. Most brokers display their server time in the Market Watch window header, so check that first. Getting this wrong means your EA will build profiles for the wrong hours, and the whole strategy falls apart.

For each session, the EA builds a volume profile. A volume profile is a histogram that shows how much volume traded at each price level. In MQL5, we don't have a built-in volume profile indicator, so we build it ourselves using a custom structure. We iterate through the bars of the session and accumulate tick volume at each price level. The result is a map of price levels to volume.

The opening range is defined as the high and low of the first ORB_Period minutes of the session. A common default is 30 minutes. Once the opening range is established, the EA waits for a breakout. When price closes beyond the range high or low, the EA checks the volume profile in the zone just beyond the breakout level. If the volume there exceeds a configurable multiple of the average session volume per level, the trade is triggered.

The EA also includes a time filter — it only trades during the session for which it's configured. If you're running it on the London session, it won't open trades during the Asian session. This prevents the EA from taking low-quality trades at the wrong time of day. You can run three instances of the EA on the same chart with different magic numbers and session settings if you want to trade all three sessions — just make sure to set a unique Magic_Number for each instance so they don't interfere with each other's positions.

Building the EA in MQL5: The Code

Let's get into the implementation. I'll walk you through the key parts of the code and then give you the full source. The EA is structured around a few core functions:

  1. BuildSessionVolumeProfile() — iterates through session bars and accumulates volume per price level.
  2. GetSessionOR() — finds the opening range high and low for the current session.
  3. CheckBreakout() — detects a breakout and validates it with volume.
  4. OnTick() — the main entry point that orchestrates everything.

Here's the full EA code. I've kept it clean and commented so you can follow the logic:

//+------------------------------------------------------------------+
//|                                          SessionVolumeProfileEA.mq5 |
//|                                      Copyright 2024, TradingBotMaker |
//+------------------------------------------------------------------+
#property copyright "TradingBotMaker"
#property version   "1.00"
#property strict

//--- Input parameters
input string Session_Name = "London";           // Session to trade: Asian, London, New York
input int    Session_Start_Hour = 8;            // Session start hour (server time)
input int    Session_Start_Minute = 0;          // Session start minute
input int    Session_End_Hour = 16;             // Session end hour (server time)
input int    Session_End_Minute = 0;            // Session end minute
input int    ORB_Period = 30;                   // Opening range period (minutes)
input double Volume_Threshold_Mult = 1.5;       // Volume confirmation multiplier
input double Risk_Percent = 1.0;                // Risk per trade (% of balance)
input double SL_ATR_Mult = 1.5;                 // Stop loss ATR multiplier
input double TP_RR_Ratio = 2.0;                 // Take profit risk-reward ratio
input int    ATR_Period = 14;                   // ATR period
input int    Magic_Number = 20240101;           // EA magic number
input bool   Use_Fixed_SL = false;              // Use fixed SL instead of ATR
input double Fixed_SL_Points = 200;             // Fixed SL in points
input bool   Close_At_Session_End = true;       // Close trades at session end

//--- Global variables
double g_sl_points, g_tp_points;
datetime g_session_start, g_session_end;
bool g_session_active = false;
bool g_orb_established = false;
double g_orb_high, g_orb_low;
double g_avg_volume_per_level;

//--- Volume profile structure
struct VolumeLevel
{
   double price;
   long   volume;
};

VolumeLevel g_volume_profile[];
int g_profile_size = 0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- Set magic number for all orders
   Trade.SetExpertMagicNumber(Magic_Number);
   
   //--- Validate session parameters
   if(ORB_Period <= 0 || Session_Start_Hour < 0 || Session_Start_Hour > 23 ||
      Session_End_Hour < 0 || Session_End_Hour > 23)
   {
      Print("Invalid session parameters. Check input values.");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   Print("Session Volume Profile EA initialized. Session: ", Session_Name);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   //--- Clean up
   ArrayFree(g_volume_profile);
   Print("Session Volume Profile EA deinitialized.");
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   //--- Check if we're in the trading session
   CheckSession();
   
   if(!g_session_active)
      return;
   
   //--- Build the volume profile if we're in the opening range period
   if(!g_orb_established)
   {
      BuildSessionVolumeProfile();
      GetSessionOR();
      
      //--- Check if the opening range period is over
      datetime orb_end = g_session_start + ORB_Period * 60;
      if(TimeCurrent() >= orb_end)
      {
         g_orb_established = true;
         Print("Opening range established: High=", g_orb_high, " Low=", g_orb_low);
         Print("Average volume per level: ", g_avg_volume_per_level);
      }
      return;
   }
   
   //--- Check for breakout
   CheckBreakout();
   
   //--- Close trades at session end if configured
   if(Close_At_Session_End && TimeCurrent() >= g_session_end)
   {
      CloseAllPositions();
   }
}

//+------------------------------------------------------------------+
//| Check if current time is within the trading session             |
//+------------------------------------------------------------------+
void CheckSession()
{
   datetime now = TimeCurrent();
   MqlDateTime dt;
   TimeToStruct(now, dt);
   
   //--- Calculate session start and end for today
   g_session_start = StringToTime(StringFormat("%04d.%02d.%02d %02d:%02d",
      dt.year, dt.mon, dt.day, Session_Start_Hour, Session_Start_Minute));
   g_session_end = StringToTime(StringFormat("%04d.%02d.%02d %02d:%02d",
      dt.year, dt.mon, dt.day, Session_End_Hour, Session_End_Minute));
   
   //--- Handle sessions that cross midnight
   if(g_session_end < g_session_start)
      g_session_end += 86400; // add one day
   
   //--- Check if we're in the session
   if(now >= g_session_start && now <= g_session_end)
   {
      if(!g_session_active)
      {
         g_session_active = true;
         g_orb_established = false;
         Print("Session started: ", Session_Name);
      }
   }
   else
   {
      if(g_session_active)
      {
         g_session_active = false;
         g_orb_established = false;
         Print("Session ended: ", Session_Name);
      }
   }
}

//+------------------------------------------------------------------+
//| Build the volume profile for the current session                |
//+------------------------------------------------------------------+
void BuildSessionVolumeProfile()
{
   //--- Clear previous profile
   ArrayFree(g_volume_profile);
   g_profile_size = 0;
   
   //--- Find the first bar of the session
   datetime session_start = g_session_start;
   int start_index = iBarShift(_Symbol, PERIOD_M1, session_start);
   
   if(start_index < 0)
   {
      Print("Error finding session start bar.");
      return;
   }
   
   //--- Iterate through bars from session start to current bar
   int current_index = 0; // current bar
   int total_bars = start_index - current_index + 1;
   
   for(int i = start_index; i >= current_index; i--)
   {
      datetime bar_time = iTime(_Symbol, PERIOD_M1, i);
      if(bar_time < session_start || bar_time > TimeCurrent())
         continue;
      
      double bar_high = iHigh(_Symbol, PERIOD_M1, i);
      double bar_low = iLow(_Symbol, PERIOD_M1, i);
      long bar_volume = iVolume(_Symbol, PERIOD_M1, i);
      
      //--- Distribute volume across price levels (simplified: use midpoint)
      double mid_price = (bar_high + bar_low) / 2;
      AddVolumeLevel(mid_price, bar_volume);
   }
   
   //--- Calculate average volume per level
   if(g_profile_size > 0)
   {
      long total_volume = 0;
      for(int i = 0; i < g_profile_size; i++)
         total_volume += g_volume_profile[i].volume;
      g_avg_volume_per_level = (double)total_volume / g_profile_size;
   }
   else
   {
      g_avg_volume_per_level = 0;
   }
}

//+------------------------------------------------------------------+
//| Add a volume level to the profile                               |
//+------------------------------------------------------------------+
void AddVolumeLevel(double price, long volume)
{
   //--- Check if the price level already exists
   for(int i = 0; i < g_profile_size; i++)
   {
      double diff = MathAbs(g_volume_profile[i].price - price);
      double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
      
      if(diff < tick_size)
      {
         g_volume_profile[i].volume += volume;
         return;
      }
   }
   
   //--- Add new level
   g_profile_size++;
   ArrayResize(g_volume_profile, g_profile_size);
   g_volume_profile[g_profile_size - 1].price = price;
   g_volume_profile[g_profile_size - 1].volume = volume;
}

//+------------------------------------------------------------------+
//| Get the opening range high and low                              |
//+------------------------------------------------------------------+
void GetSessionOR()
{
   //--- Find the first bar of the session
   int start_index = iBarShift(_Symbol, PERIOD_M1, g_session_start);
   int orb_bars = ORB_Period;
   
   if(start_index < 0 || start_index < orb_bars - 1)
   {
      Print("Not enough bars for opening range.");
      return;
   }
   
   //--- Find high and low over the opening range period
   g_orb_high = iHigh(_Symbol, PERIOD_M1, start_index);
   g_orb_low = iLow(_Symbol, PERIOD_M1, start_index);
   
   for(int i = start_index; i > start_index - orb_bars; i--)
   {
      double bar_high = iHigh(_Symbol, PERIOD_M1, i);
      double bar_low = iLow(_Symbol,

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles