Why Most Scalping EAs Miss the Real Liquidity Picture
You've seen it before: a scalping EA that fires entries based on a moving average cross or an RSI divergence, only to get stopped out because price slammed through a level where nobody was actually willing to buy. The EA's logic was sound on the chart, but the market's order book told a different story.
I've been there. After burning through a few accounts with "optimized" scalpers that worked in backtest but failed live, I started digging into what the MT5 Market Depth (DOM) actually shows. It's not a magic bullet, but it's the closest thing to seeing the battlefield before the fight starts. For scalping, where you're in and out within a handful of ticks, knowing where the real liquidity sits is worth more than any lagging indicator. I remember one painful week where a "perfect" MA-cross scalper took 12 consecutive losses during a news event — the DOM would have shown the ask wall at 1.1050 that never budged, but the EA kept buying into it.
This post is for the developer who wants to build an MQL5 scalping EA that reads the order book and makes decisions based on actual supply and demand, not just price history. I'll walk through what liquidity levels look like in the DOM, how to interpret them, and — most importantly — how to code a simple EA that uses them without overcomplicating things. You won't get a ready-to-deploy EA here, but you'll get the core logic and the traps to avoid.
What the MT5 DOM Actually Tells You
Open the Market Depth window in MT5 (right-click a symbol in the Market Watch → "Depth of Market" or press Ctrl+B). You'll see two sides: Bid (buyers) and Ask (sellers), with volumes stacked at each price level. The top of the Bid side shows the highest price someone is willing to pay; the top of the Ask side shows the lowest price someone is willing to sell at. The spread sits between them.
For a scalper, the key isn't the top level. It's the clusters of volume a few ticks away. A liquidity level appears when a price point has significantly more volume than surrounding levels — sometimes 5x or 10x the average. These are the zones where large participants have placed limit orders. They act as magnets or walls, depending on context. In practice, I've watched EURUSD during London open where a 200-lot bid at 1.0870 held price for over 40 minutes before finally absorbing the selling pressure.
Reading Clusters vs. Walls
Not all volume is equal. A wall is a single price level with massive volume — think 500+ lots on a forex pair like EURUSD. That's usually a resting order from a bank or institution. Price will often test it, bounce, or absorb it. A cluster is a broader zone of accumulated volume across 3-5 price levels, each with moderate size. Clusters indicate a range where multiple participants are comfortable adding size. These are more reliable for scalping because they represent consensus rather than a single entity's order.
I've found that clusters on the Ask side (above current price) act as resistance for short-term moves, while clusters on the Bid side (below price) act as support. When price approaches a cluster, you often see acceleration or rejection, depending on whether the cluster is being consumed or defended. A wall, by contrast, can vanish instantly if the owner cancels — I've seen a 600-lot ask wall at 1.0920 disappear within 200 milliseconds, right before price shot through. Clusters are harder to spoof because they require coordinated volume across multiple prices.
The DOM Data Structure in MQL5
Before we code, you need to understand how MT5 exposes the DOM. The MqlBookInfo structure contains these fields:
| Field | Type | Description |
|---|---|---|
| price | double | Price level of the order |
| volume | long | Volume in lots (or units, depending on broker) |
| type | ENUM_BOOK_TYPE | BOOK_TYPE_BUY or BOOK_TYPE_SELL |
| volume_at | long | Volume at this price from the current moment (same as volume in most cases) |
One gotcha: MarketBookGet returns the entire order book snapshot, but the array is not sorted by price in a guaranteed order. You'll need to sort it yourself if order matters. I usually sort by price ascending for the ask side and descending for the bid side, but for cluster detection, unsorted works fine since we check each element by type.
Building a Scalping EA That Reads the DOM
MT5's MarketBookGet and MarketBookInfo functions let you access the DOM programmatically. The tricky part is that the DOM updates asynchronously — you need to handle the SYMBOL_BOOKTICK event or poll at a high frequency. For scalping, I prefer polling inside OnTick because the event-driven approach can miss rapid changes if your EA is busy with other logic. The downside is more CPU usage, but on a modern VPS, it's fine.
Here's a minimal example that fetches the current DOM and identifies the largest volume level on each side:
//+------------------------------------------------------------------+
//| Get DOM liquidity levels |
//+------------------------------------------------------------------+
void GetLiquidityLevels(double &bidLevel, double &askLevel, long &bidVol, long &askVol)
{
MqlBookInfo book[];
if(!MarketBookGet(_Symbol, book))
{
Print("Failed to get market book: ", GetLastError());
return;
}
long maxBidVol = 0, maxAskVol = 0;
double maxBidPrice = 0, maxAskPrice = 0;
for(int i = 0; i < ArraySize(book); i++)
{
if(book[i].type == BOOK_TYPE_BUY) // Bid side
{
if(book[i].volume > maxBidVol)
{
maxBidVol = book[i].volume;
maxBidPrice = book[i].price;
}
}
else if(book[i].type == BOOK_TYPE_SELL) // Ask side
{
if(book[i].volume > maxAskVol)
{
maxAskVol = book[i].volume;
maxAskPrice = book[i].price;
}
}
}
bidLevel = maxBidPrice;
askLevel = maxAskPrice;
bidVol = maxBidVol;
askVol = maxAskVol;
}This gives you the price with the highest volume on each side. For a scalper, you'd then check if current price is within, say, 5 pips of that level. If it's approaching a heavy bid level from above, you might look to buy. If it's hitting a heavy ask level from below, you might short. But that's just the starting point. The single-level approach is naive — it'll trigger on spoof orders constantly.
Cluster Detection Logic
A single large level can be a trap — a spoof order that gets pulled right before price reaches it. Clusters are more robust. Here's how I detect them:
- Iterate through the DOM and group consecutive price levels where volume exceeds a threshold (e.g., 3x the average volume of the last 10 levels).
- If at least 3 consecutive levels meet the threshold, mark that price range as a cluster.
- Track the total volume in the cluster and its midpoint price.
In code, that looks something like:
//+------------------------------------------------------------------+
//| Detect volume clusters |
//+------------------------------------------------------------------+
bool DetectCluster(MqlBookInfo &book[], double &clusterPrice, long &clusterVol, int side)
{
// side: 0 = Bid, 1 = Ask
long avgVol = 0;
int count = ArraySize(book);
for(int i = 0; i < count; i++) avgVol += book[i].volume;
avgVol /= count;
if(avgVol == 0) return false;
long threshold = avgVol * 3;
int consecutive = 0;
double sumPrice = 0;
long sumVol = 0;
for(int i = 0; i < count; i++)
{
bool isSide = (side == 0 && book[i].type == BOOK_TYPE_BUY) ||
(side == 1 && book[i].type == BOOK_TYPE_SELL);
if(!isSide) continue;
if(book[i].volume >= threshold)
{
consecutive++;
sumPrice += book[i].price * book[i].volume;
sumVol += book[i].volume;
}
else
{
if(consecutive >= 3)
{
clusterPrice = sumPrice / sumVol;
clusterVol = sumVol;
return true;
}
consecutive = 0;
sumPrice = 0;
sumVol = 0;
}
}
// Check at end
if(consecutive >= 3)
{
clusterPrice = sumPrice / sumVol;
clusterVol = sumVol;
return true;
}
return false;
}This returns the volume-weighted average price of the cluster and the total volume. You'd call it for both the Bid and Ask sides each tick. One edge case: if the DOM only has 2 levels total (some brokers limit depth), this function will never return true because consecutive >= 3 fails. Handle that by checking ArraySize(book) first — if it's below 5, fall back to single-level logic or skip trading entirely.
Handling DOM Update Frequency
The DOM updates asynchronously. In OnTick, you might get the same snapshot multiple times. To avoid redundant processing, track the last snapshot timestamp using MarketBookInfo(SYMBOL_BOOKTICK). Here's a simple guard:
datetime lastBookTime = 0;
void OnTick()
{
long bookTick = MarketBookInfo(SYMBOL_BOOKTICK);
if(bookTick == lastBookTime) return; // No change
lastBookTime = bookTick;
// Process DOM...
}This prevents your EA from recomputing clusters on every tick when the DOM hasn't changed. For high-frequency scalping, this matters — you don't want to waste CPU cycles on redundant calculations.
Integrating DOM Signals Into a Scalping Strategy
Let's be honest: the DOM alone isn't a strategy. It's a filter. I combine it with a simple price action rule: trade in the direction of the nearest cluster that's being defended. If price is approaching a bid cluster from above and the volume at that cluster isn't shrinking (i.e., orders aren't being pulled), I look to buy. If the cluster starts evaporating (volume drops rapidly), I stay out.
Here's a practical parameter set I've used in testing:
<tr style="| Parameter | Type | Default | Description |
|---|---|---|---|
| ClusterVolumeMultiplier | double | 3.0 | Multiplier of average volume to define a cluster threshold. |






