Why You Need a Spread Monitor in the First Place
Every scalper has that one trade that looked perfect on the chart, only to get filled at a spread that ate half the profit before the position even opened. I've been there. You see EURUSD at 0.1 pips on your demo account, switch to a live account with the same broker, and suddenly it's 1.2 pips during London lunch. The chart doesn't tell you that. Your EA doesn't tell you that. And by the time you realize what happened, you're already in the trade.
A standalone MQL5 spread monitor EA solves this by giving you a real-time view of what's actually happening across your symbols — not what the chart suggests, but what the broker is quoting right now. It's a utility, not a trading strategy. It doesn't open positions, doesn't manage risk, and doesn't pretend to predict price. What it does is watch the spread on every symbol you care about and alert you the moment things go sideways.
This article walks through building exactly that: a spread alert MQL5 utility with an on-chart dashboard, configurable thresholds per symbol, and multiple alert channels. You'll get the full source code, the reasoning behind each design choice, and honest notes on where this kind of tool helps — and where it doesn't.
What a Spread Monitor Actually Does
The concept is simple: read the current bid and ask for a symbol, subtract them, convert to pips, and display the result. But the devil is in the details, and that's where most naive implementations fall apart.
First, spread isn't static. It widens during news events, narrows during quiet Asian sessions, and can spike to absurd levels during rollover or liquidity gaps. A monitor that just shows the current value is only marginally useful. What you need is a spread threshold EA that compares the live spread against a configurable maximum and fires an alert when it breaches that level.
Second, you need to handle the difference between points and pips. In MQL5, the SymbolInfoInteger function returns spread in points, not pips. For a 5-digit broker, that means a spread of 10 points is actually 1.0 pips. Getting this conversion wrong is the single most common bug I see in spread-related code. Your monitor needs to know the symbol's digits and point value to display something meaningful.
Third, you need to decide what to do when the spread exceeds the threshold. A real-time alert is the minimum. But a useful utility also logs the event, tracks how long the spread stayed elevated, and gives you a visual indicator on the dashboard so you can see at a glance which symbols are problematic.
Why Not Just Use the Broker's Built-In Tools?
MetaTrader 5 has a Market Watch window that shows spread for visible symbols. That's fine if you're watching one or two pairs and you're glued to the screen. But it doesn't alert you, it doesn't track history, and it doesn't let you set per-symbol thresholds. If you're running an EA overnight or you're away from the desk, a static window is useless.
There are also third-party spread indicators on the MQL5 marketplace. Some are decent. Most are overpriced for what they do, and none of them let you customize the alert logic to fit your specific workflow. Building your own spread dashboard MT5 utility takes about an hour and gives you complete control over every aspect of the behavior.
Building the MQL5 Spread Monitor EA
Let's get into the code. I'm going to show you a complete, working implementation that you can compile in MetaEditor and attach to any chart. The utility uses an OnTimer event to poll spread values at a configurable interval, draws a dashboard panel using chart objects, and fires alerts through multiple channels.
Input Parameters
Before writing the logic, we need to define what the user can configure. Here's the input block:
//+------------------------------------------------------------------+
//| Input parameters |
//+------------------------------------------------------------------+
input string SymbolList = "EURUSD,GBPUSD,USDJPY,XAUUSD";
input int UpdateIntervalSeconds = 2;
input double DefaultMaxSpreadPips = 2.5;
input bool EnableAlerts = true;
input bool EnablePushNotifications = false;
input bool EnableEmailAlerts = false;
input bool LogToFile = true;
input int AlertCooldownSeconds = 60;
input color NormalColor = C'0,153,68';
input color WarningColor = C'255,153,0';
input color CriticalColor = C'204,0,0';
Let me explain the design decisions here. SymbolList is a comma-separated string rather than a fixed array because that's far easier for users to edit without recompiling. UpdateIntervalSeconds defaults to 2 — fast enough to catch spikes but not so fast that it hammers the terminal with symbol info requests. DefaultMaxSpreadPips applies to any symbol not explicitly listed in a per-symbol override map. The alert channels are all optional because not everyone wants push notifications at 3 AM.
The AlertCooldownSeconds parameter is critical. Without it, a symbol stuck at a wide spread would fire an alert every single tick, flooding your screen and making the utility useless. The cooldown ensures you get one notification per event window, not a continuous stream of noise.
Data Structures and Initialization
We need a way to track state for each symbol: the current spread, whether an alert is active, and the last time we fired an alert. A simple struct handles this cleanly:
//+------------------------------------------------------------------+
//| Symbol tracking structure |
//+------------------------------------------------------------------+
struct SymbolData
{
string symbol;
double currentSpreadPips;
double maxSpreadPips;
bool alertActive;
datetime lastAlertTime;
};
SymbolData g_symbols[];
int g_symbolCount = 0;
In OnInit, we parse the comma-separated symbol list, validate each symbol exists, and populate the array. Here's the initialization logic:
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
string symbols[];
int count = StringSplit(SymbolList, ',', symbols);
if(count == 0)
{
Print("ERROR: No symbols specified in SymbolList.");
return INIT_PARAMETERS_INCORRECT;
}
ArrayResize(g_symbols, count);
g_symbolCount = 0;
for(int i = 0; i < count; i++)
{
string sym = TrimSymbol(symbols[i]);
if(sym == "" || !SymbolSelect(sym, true))
{
PrintFormat("WARNING: Symbol '%s' not available, skipping.", sym);
continue;
}
g_symbols[g_symbolCount].symbol = sym;
g_symbols[g_symbolCount].currentSpreadPips = 0.0;
g_symbols[g_symbolCount].maxSpreadPips = DefaultMaxSpreadPips;
g_symbols[g_symbolCount].alertActive = false;
g_symbols[g_symbolCount].lastAlertTime = 0;
g_symbolCount++;
}
if(g_symbolCount == 0)
{
Print("ERROR: No valid symbols to monitor.");
return INIT_PARAMETERS_INCORRECT;
}
EventSetTimer(UpdateIntervalSeconds);
DrawDashboard();
PrintFormat("Spread Monitor initialized. Tracking %d symbols.", g_symbolCount);
return INIT_SUCCEEDED;
}
Note the SymbolSelect call — this adds the symbol to Market Watch, which is a prerequisite for reading its bid/ask prices. If a symbol isn't in Market Watch, SymbolInfoDouble will return zeros or fail entirely.
Core Spread Calculation
The heart of the utility is the UpdateSpread function. This reads the current bid and ask, calculates the spread in points, converts to pips, and updates the struct:
//+------------------------------------------------------------------+
//| Update spread for a single symbol |
//+------------------------------------------------------------------+
void UpdateSpread(int index)
{
string sym = g_symbols[index].symbol;
double bid = SymbolInfoDouble(sym, SYMBOL_BID);
double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
if(bid == 0.0 || ask == 0.0)
{
g_symbols[index].currentSpreadPips = -1.0; // no quote available
return;
}
double spreadPoints = ask - bid;
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
// Convert to pips — handle 3/5-digit symbols correctly
double pipSize = (digits == 3 || digits == 5) ? point * 10 : point;
g_symbols[index].currentSpreadPips = spreadPoints / pipSize;
}
That pip conversion deserves a moment of attention. A 5-digit broker quotes EURUSD at 1.08523/1.08525, so the spread is 2 points. The point value is 0.00001, and since we have 5 digits, a pip is 10 points. Dividing 2 points by 10 gives 0.2 pips — correct. For USDJPY at 3 digits (e.g., 148.523/148.527), the spread is 4 points, the point is 0.001, and a pip is 10 points, giving 0.4 pips. Also correct. Getting this wrong is how you end up alerting at 10x the intended threshold.
Alert Logic with Cooldown
Now for the part that actually makes this a spread alert MQL5 tool rather than just a display. The alert logic checks whether the current spread exceeds the threshold, respects the cooldown period, and fires the appropriate alert channels:
//+------------------------------------------------------------------+
//| Check spread against threshold and fire alerts |
//+------------------------------------------------------------------+
void CheckThreshold(int index)
{
SymbolData &s = g_symbols[index];
if(s.currentSpreadPips < 0)
return; // no quote, skip
bool overThreshold = (s.currentSpreadPips > s.maxSpreadPips);
if(overThreshold && !s.alertActive)
{
// Check cooldown
if(TimeCurrent() - s.lastAlertTime >= AlertCooldownSeconds)
{
FireAlert(s.symbol, s.currentSpreadPips, s.maxSpreadPips);
s.lastAlertTime = TimeCurrent();
s.alertActive = true;
}
}
else if(!overThreshold && s.alertActive)
{
// Spread back to normal
s.alertActive = false;
PrintFormat("INFO: Spread on %s back to normal (%.1f pips).",
s.symbol, s.currentSpreadPips);
}
}
The alertActive flag prevents repeated alerts while the spread stays elevated. Combined with the cooldown, you get one alert when the spread first breaches, then silence until either the cooldown expires (and it's still breached) or the spread recovers. This is the behavior I've found most useful in practice — it tells you about the problem without nagging you every second.
Alert Channels
Here's the FireAlert function that routes the notification through whatever channels the user enabled:
//+------------------------------------------------------------------+
//| Fire alert through configured channels |
//+------------------------------------------------------------------+
void FireAlert(string symbol, double spread, double threshold)
{
string msg = StringFormat(
"SPREAD ALERT: %s spread is %.1f pips (threshold: %.1f pips)",
symbol, spread, threshold
);
if(EnableAlerts)
Alert(msg);
if(EnablePushNotifications)
SendNotification(msg);
if(EnableEmailAlerts)
SendMail("MQL5 Spread Monitor Alert", msg);
if(LogToFile)
{
int handle = FileOpen("SpreadMonitor.log", FILE_WRITE|FILE_READ|FILE_TXT);
if(handle != INVALID_HANDLE)
{
FileSeek(handle, 0, SEEK_END);
FileWrite(handle, TimeToString(TimeCurrent()), symbol,
DoubleToString(spread, 1), DoubleToString(threshold, 1));
FileClose(handle);
}
}
}
One note on SendMail: it only works if you've configured an email address in Tools → Options → Email in MetaTrader 5. The same goes for SendNotification — you need the MetaQuotes ID from the mobile app set up in Tools → Options → Notifications. These are terminal-level settings, not something the EA can configure for you. I've seen users complain that email alerts don't work when the real issue is they never set up the mail server in the terminal.
The Dashboard Panel
The spread dashboard MT5 panel is what makes this utility pleasant to use. I draw it using chart objects — a background rectangle, text labels for headers, and per-symbol rows. Here's the core of the drawing logic:
//+------------------------------------------------------------------+
//| Draw the dashboard panel |
//+------------------------------------------------------------------+
void DrawDashboard()
{
int rows = g_symbolCount + 2; // header + symbols + footer
int rowHeight = 22;
int panelWidth = 320;
int panelHeight = rows * rowHeight + 10;
string bgName = "SpreadMonitor_BG";
ObjectCreate(0, bgName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, bgName, OBJPROP_XDISTANCE, 20);
ObjectSetInteger(0, bgName, OBJPROP_YDISTANCE, 20);
ObjectSetInteger(0, bgName, OBJPROP_XSIZE, panelWidth);
ObjectSetInteger(0, bgName, OBJPROP_YSIZE, panelHeight);
ObjectSetInteger(0, bgName, OBJPROP_BGCOLOR, C'20,30,40');
ObjectSetInteger(0, bgName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, bgName, OBJPROP_COLOR, C'200,200,200');
ObjectSetInteger(0, bgName, OBJPROP_BACK, false);
ObjectSetInteger(0, bgName, OBJPROP_SELECTABLE, false);
// Header labels
CreateLabel("SpreadMonitor_Title", "Symbol", 30, 25, NormalColor);
CreateLabel("SpreadMonitor_Spread", "Spread", 150, 25, NormalColor);
CreateLabel("SpreadMonitor_Status", "Status", 260, 25, NormalColor);
// Per-symbol rows
for(int i = 0; i < g_symbolCount; i++)
{
int yPos = 50 + i * rowHeight;
string symName = "SpreadMonitor_Sym_" + IntegerToString(i);
string sprName = "SpreadMonitor_Spr_" + IntegerToString(i);
string stsName = "SpreadMonitor_Sts_" + IntegerToString(i);
CreateLabel(symName, g_symbols[i].symbol, 30, yPos, clrWhite);
CreateLabel(sprName, "--", 150, yPos, clrWhite);
CreateLabel(stsName, "OK", 260, yPos, NormalColor);
}
}
The CreateLabel helper is just a wrapper around ObjectCreate with OBJ_LABEL and the appropriate font settings. I won't reproduce it here in full, but it sets OBJPROP_FONTSIZE to 10, OBJPROP_CORNER to CORNER_LEFT_UPPER, and OBJPROP_SELECTABLE to false so the labels don't interfere with chart interaction.
The panel updates in OnTimer, which runs every UpdateIntervalSeconds. Each tick, we call UpdateSpread for every symbol, update the label text, and change the status color based on the spread relative to the threshold:
- Green — spread is below 80% of the threshold
- Orange — spread is between 80% and 100% of the threshold
- Red — spread exceeds the threshold
This three-tier color scheme gives





