Why Most Scalpers Ignore the DOM — and Why You Shouldn't
Every time I watch a scalper stare at a one-minute chart with a few moving averages, I cringe a little. Not because that setup can't work — it can, on the right day — but because they're ignoring the single richest source of short-term price information in MetaTrader 5: the Depth of Market (DOM).
MT5 gives you Level 2 data straight from the broker's order book. Most retail traders never open that window. The ones who do usually just glance at the bid/ask imbalance and move on. But if you're building an order flow indicator MT5 for scalping, the DOM is your raw material. You can code it, backtest it, and automate decisions based on what real market participants are doing — or at least what your broker's feed tells you they're doing.
I've spent the last few years writing MQL5 tools that read the order book tick by tick. This post walks you through building your own DOM-based scalping indicator — the logic, the code, the pitfalls, and why most "order flow" indicators you see on the market are lying to you. I'll be honest about what works and what's just smoke and mirrors.
What the DOM Actually Tells You (and What It Doesn't)
The Depth of Market window in MT5 shows pending limit orders at different price levels — bids on the left, asks on the right. Each row shows the cumulative volume waiting at that price. When you see 500 lots stacked at 1.1050 on the bid side, that's real resting liquidity. At least, it's what your broker wants you to see.
For a scalper, the key signals are:
- Imbalance: Heavy volume on one side suggests the market may move toward the thinner side as the thick side acts as support or resistance. If there's 800 contracts on the bid at 1.1050 and only 200 on the ask at 1.1051, price is more likely to bounce up than break down — assuming the bid wall holds.
- Iceberg orders: When volume disappears and reappears at the same price, a large player is hiding their full size. You'll see 100 contracts, then 100 again after a fill, then 100 again. This often precedes a breakout because someone is accumulating a position without revealing their hand.
- Absorption: Price keeps hitting a level but the volume there doesn't shrink — someone is absorbing all the sell orders. That's accumulation. The DOM shows the same bid volume persisting even as price tests it repeatedly. Smart money is buying into weakness.
- Speed of change: How fast the DOM updates tells you if momentum is accelerating or fading. A DOM that's flickering rapidly with large volume changes signals high participation. A slow, stagnant DOM means the market is asleep.
But here's the uncomfortable truth: the DOM you see is delayed by the broker's feed, often by 100-300 milliseconds. That's an eternity in scalping. You're not seeing the real order book — you're seeing a snapshot that's already slightly stale. A good MT5 order flow strategy accounts for this lag by looking at rate of change, not absolute levels. The direction of change matters more than the raw numbers.
Another dirty secret: some brokers manipulate their DOM feed. Market makers especially will show synthetic depth that doesn't reflect actual interbank liquidity. I've tested DOM feeds from six brokers on the same symbol at the same time — the numbers were different for every single one. Your DOM is not the market's DOM. It's your broker's filtered view.
Building the Order Flow Indicator in MQL5
Let's get into the code. I'll show you the core structure of a DOM-based scalping indicator that tracks cumulative bid/ask volume imbalance over a rolling window. This isn't a complete EA — it's the data engine you'd plug into your own strategy. You can extend it with entry logic, money management, and trade execution.
Prerequisites
You need a broker that provides Depth of Market data in MT5. Most ECN brokers do, but some market makers only show synthetic depth. Check by opening View → Market Watch, right-click any symbol, select Depth of Market. If you see real numbers changing tick by tick, you're good. If the numbers are static or only update every few seconds, that broker's DOM is useless for scalping.
Your indicator will use the MarketBookAdd and MarketBookGet functions. These are MT5-specific — MT4 has no DOM API whatsoever. If you're still on MT4, you cannot build this indicator natively. You'd need to use a third-party bridge or switch to MT5.
Indicator Structure
Here's the skeleton with the key components. I'm using four buffers: two for the raw volumes and two for the plotted imbalance histograms. The #property directives set up the chart appearance — green for bid-heavy, red for ask-heavy.
//+------------------------------------------------------------------+
//| OrderFlowScalper.mq5 |
//| Copyright 2025, Your Name Here |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots 2
// Plot 1: Bid volume imbalance
#property indicator_label1 "BidImbalance"
#property indicator_type1 DRAW_HISTOGRAM
#property indicator_color1 clrLimeGreen
#property indicator_width1 2
// Plot 2: Ask volume imbalance
#property indicator_label2 "AskImbalance"
#property indicator_type2 DRAW_HISTOGRAM
#property indicator_color2 clrRed
#property indicator_width2 2
// Buffers
double BidImbalanceBuffer[];
double AskImbalanceBuffer[];
double RawBidVolume[];
double RawAskVolume[];
// Inputs
input int DepthLevels = 10; // Number of DOM levels to scan
input int LookbackBars = 50; // Rolling window for normalization
input double ImbalanceThresh = 1.5; // Trigger ratio (e.g., 1.5 = 50% more volume on one side)
// DOM handle
bool domSubscribed = false;
The key decision here is DepthLevels. I set it to 10 by default — scanning more than 20 levels adds noise because far-away limit orders rarely get hit in a scalping timeframe. If you're trading 5-minute bars, level 30 is almost irrelevant. Price would have to move 15-20 pips to reach those levels, and by then your scalp thesis is dead.
I've tested DepthLevels at 5, 10, 15, and 25 on EURUSD during London session. The 5-level version was too reactive — it triggered on every tiny imbalance. The 25-level version was sluggish and missed fast moves. Ten levels hit the sweet spot for 1-minute scalping.
Reading the Order Book
The OnCalculate function calls a helper that grabs the current DOM snapshot. Notice I subscribe to the DOM only once using a static boolean flag. Subscribing every tick would waste resources and potentially get you rate-limited by the broker.
//+------------------------------------------------------------------+
//| Read DOM and compute imbalance |
//+------------------------------------------------------------------+
bool GetDOMImbalance(double &bidVol, double &askVol)
{
if(!domSubscribed)
{
if(!MarketBookAdd(_Symbol))
{
Print("Failed to subscribe to DOM for ", _Symbol);
return false;
}
domSubscribed = true;
}
MqlBookInfo book[];
if(!MarketBookGet(_Symbol, book))
{
return false;
}
bidVol = 0;
askVol = 0;
int count = ArraySize(book);
for(int i = 0; i < count && i < DepthLevels; i++)
{
if(book[i].type == BOOK_TYPE_BUY) // Bid side
bidVol += book[i].volume;
else if(book[i].type == BOOK_TYPE_SELL) // Ask side
askVol += book[i].volume;
}
return true;
}
Notice I'm summing only the top DepthLevels entries. The order book array from MarketBookGet is sorted by proximity to the current price, so the first entries are the closest levels. That's exactly what we want — the liquidity that price will hit first. The MqlBookInfo structure contains price, volume, and type fields. The type field uses BOOK_TYPE_BUY for bids and BOOK_TYPE_SELL for asks.
One edge case: if MarketBookGet returns an empty array (size zero), the function returns false and the indicator doesn't update. This can happen during low liquidity periods or if the broker's feed drops. I handle this by simply skipping that tick — better to miss a signal than to plot garbage data.
Computing the Imbalance Signal
Now we normalize the raw volumes using a simple ratio. I prefer a ratio because it's more intuitive for scalping than a Z-score. A ratio of 2.0 means there's twice as much volume on one side. You can visualize that immediately without mental math.
//+------------------------------------------------------------------+
//| Inside OnCalculate |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(prev_calculated == 0)
{
ArrayInitialize(BidImbalanceBuffer, 0);
ArrayInitialize(AskImbalanceBuffer, 0);
ArrayInitialize(RawBidVolume, 0);
ArrayInitialize(RawAskVolume, 0);
}
// Only update on new tick (not every bar)
static datetime lastTime = 0;
if(lastTime == time[rates_total - 1])
return(rates_total);
lastTime = time[rates_total - 1];
double bidVol, askVol;
if(!GetDOMImbalance(bidVol, askVol))
return(rates_total);
// Store raw volumes
RawBidVolume[rates_total - 1] = bidVol;
RawAskVolume[rates_total - 1] = askVol;
// Compute ratio (avoid division by zero)
double ratio = (askVol > 0) ? bidVol / askVol : 3.0; // Cap at 3.0
if(bidVol == 0 && askVol == 0) ratio = 1.0;
if(ratio > ImbalanceThresh)
{
BidImbalanceBuffer[rates_total - 1] = ratio;
AskImbalanceBuffer[rates_total - 1] = 0;
}
else if(ratio < 1.0 / ImbalanceThresh)
{
BidImbalanceBuffer[rates_total - 1] = 0;
AskImbalanceBuffer[rates_total - 1] = 1.0 / ratio;
}
else
{
BidImbalanceBuffer[rates_total - 1] = 0;
AskImbalanceBuffer[rates_total - 1] = 0;
}
return(rates_total);
}
This gives you a histogram that lights up green when bid volume dominates (buyers are stacking limit orders) and red when ask volume dominates (sellers are stacking). The ImbalanceThresh of 1.5 means you need 50% more volume on one side to trigger a signal. I arrived at that number after testing on EURUSD and GBPUSD — lower values gave too many false signals during news events. Higher values like 2.0 made the indicator silent for hours at a time.
One nuance: I cap the ratio at 3.0 to avoid extreme spikes when one side has near-zero volume. Without this cap, a single tick with 100 contracts on bid and 1 contract on ask would show a ratio of 100, which would distort the histogram scaling. The cap keeps the visual representation readable.
Turning the Indicator into a Scalping Signal
Raw imbalance isn't a trade signal. Here's how I use it in practice:
- Green bar appears (bid heavy) → price is likely to find support at current levels. Wait for price to test the bid wall and bounce. Enter long on the bounce with a stop below the wall. Don't buy the first touch — let the market confirm the support holds.
- Red bar appears (ask heavy) → price is likely to face resistance. Wait for rejection, then short. The rejection pattern is a candlestick with a long upper wick or a close below the ask wall level.
- No bar → imbalance is neutral. Don't trade. The market is balanced and any scalp is a coin flip. This is the hardest rule to follow — most traders feel they need to be in a trade at all times. You don't.
I add one filter: only take the trade if the imbalance bar appears within 2 ticks of a support or resistance level from the higher timeframe (e.g., a 5-minute pivot). This prevents you from buying into thin air just because the DOM looks lopsided for a second. The confluence of DOM imbalance and a horizontal level dramatically increases the probability of a reversal.
Real Settings and Backtest Notes
Here's what I use in my own DOM scalping MQL5 setup after months of forward testing on a demo account. Remember: you cannot backtest DOM data in the Strategy Tester. These settings are derived from live observation, not historical optimization.
| Parameter | Value | Why This Value |
|---|---|---|
| DepthLevels | 10 | More levels add noise; top 10 capture the immediate liquidity that matters for a 5-10 pip scalp. |
| LookbackBars | 50 | Enough to establish a baseline without being too slow to adapt to regime changes. |
| ImbalanceThresh | 1.5 | Filters noise; 1. |






