Skip to content

Adding Alerts & Push Notifications

Add sound alerts, pop-up notifications, push notifications, and email alerts to your indicators.

💡
Before you start

A C++ compiler and a terminal. No MetaTrader, no broker, no account, no money, and nothing is sent anywhere. Most Linux machines already have g++; on macOS run xcode-select --install once; on Windows use the Windows Subsystem for Linux or MSYS2.

If you have done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.

No notification is actually sent by anything below. The alert function prints to the terminal instead, so you can count what would have been sent — which is the whole exercise.

Why Alerts Matter

An indicator that requires you to watch the chart constantly defeats its purpose. Alerts let your indicator notify you when conditions are met — so you can step away from the screen and still catch every signal.

MQL5 provides four alert delivery methods, from simple on-screen pop-ups to mobile push notifications.

Alert Types in MQL5

1. Pop-up Alert — A dialog box with a sound. The simplest and most common.

Alert("EURUSD: RSI crossed above 70 - Overbought!");

You can pass multiple arguments and they will be concatenated:

Alert(_Symbol, " | ", EnumToString(_Period), " | RSI: ",
      DoubleToString(rsiValue, 1), " - OVERBOUGHT");

2. Sound Alert — Play a .wav file without a dialog box. Less intrusive.

PlaySound("alert.wav");  // plays from MQL5/Sounds/ folder

3. Push Notification — Sends to your phone via the MetaTrader mobile app. You must configure your MetaQuotes ID in MT5 (Tools > Options > Notifications).

SendNotification("EURUSD Buy Signal - RSI: " + DoubleToString(rsiValue, 1));

4. Email Alert — Sends an email. Requires SMTP configuration in MT5 (Tools > Options > Email).

SendMail("Trading Alert: EURUSD",
         "Buy signal detected.
RSI: " + DoubleToString(rsiValue, 1) +
         "
Time: " + TimeToString(TimeCurrent()));

Preventing Duplicate Alerts

The biggest mistake in alert coding: firing the same alert hundreds of times because the condition remains true across multiple ticks. You must ensure each alert fires once per signal.

// Method 1: Track the last alert bar
datetime lastAlertBar = 0;

void CheckAlert(double rsiValue, datetime barTime)
{
    if(barTime == lastAlertBar) return;  // already alerted this bar

    if(rsiValue > 70)
    {
        Alert(_Symbol, " RSI Overbought: ", DoubleToString(rsiValue, 1));
        lastAlertBar = barTime;
    }
    else if(rsiValue < 30)
    {
        Alert(_Symbol, " RSI Oversold: ", DoubleToString(rsiValue, 1));
        lastAlertBar = barTime;
    }
}
// Method 2: Track the signal state (better for crossover signals)
bool wasAbove70 = false;

void CheckCrossAlert(double rsiValue)
{
    bool isAbove70 = (rsiValue > 70);

    // Alert only on the TRANSITION from below to above
    if(isAbove70 && !wasAbove70)
    {
        Alert(_Symbol, " RSI crossed above 70");
    }
    // Alert on transition from above to below
    if(!isAbove70 && wasAbove70)
    {
        Alert(_Symbol, " RSI crossed below 70");
    }

    wasAbove70 = isAbove70;
}

Making Alerts Configurable

Professional indicators let users choose which alert types they want:

input bool EnablePopupAlert = true;   // Show Pop-up Alerts
input bool EnableSoundAlert = true;   // Play Sound
input bool EnablePushAlert  = false;  // Send Push Notification
input bool EnableEmailAlert = false;  // Send Email
input string AlertSound = "alert.wav"; // Alert Sound File

void FireAlert(string message)
{
    if(EnablePopupAlert)
        Alert(message);
    if(EnableSoundAlert)
        PlaySound(AlertSound);
    if(EnablePushAlert)
        SendNotification(message);
    if(EnableEmailAlert)
        SendMail("MT5 Alert: " + _Symbol, message);
}

Alert with Price Level

A practical pattern — alert when price crosses a moving average:

input bool AlertOnCross = true;  // Alert on MA Cross

datetime lastCrossAlert = 0;
bool wasPriceAboveMA = false;

void CheckMACrossAlert(double price, double maValue, datetime barTime)
{
    if(!AlertOnCross) return;
    if(barTime == lastCrossAlert) return;

    bool isPriceAboveMA = (price > maValue);

    if(isPriceAboveMA && !wasPriceAboveMA)
    {
        FireAlert(_Symbol + " Price crossed ABOVE MA " +
                  DoubleToString(maValue, _Digits));
        lastCrossAlert = barTime;
    }
    else if(!isPriceAboveMA && wasPriceAboveMA)
    {
        FireAlert(_Symbol + " Price crossed BELOW MA " +
                  DoubleToString(maValue, _Digits));
        lastCrossAlert = barTime;
    }

    wasPriceAboveMA = isPriceAboveMA;
}

Build an Alert People Do Not Switch Off, in Five Steps

An alert has exactly one job: to be worth looking at. Almost every home-made one fails at that within a day, because it fires on every tick the condition is true rather than once per event — and after the twentieth buzz for a single crossover it gets disabled and never trusted again. In the next half hour you will measure that failure, find the second guard that most people miss, and end with one small function that covers both. Every line of output below came from running these files.

1
Save the header that supplies MetaTrader's built-ins

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as mql5.h. If you already have it from an earlier page, reuse that copy and skip to step 3.

// mql5.h - a small shim that lets a useful SUBSET of MQL5 compile and run
// with an ordinary C++ compiler, so you can practise the language without
// MetaTrader. It is NOT MetaTrader and does not trade anything.
#pragma once
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

typedef std::string string;
typedef long long datetime;
#define input static

template <typename T> int ArraySize(const std::vector<T> &a) { return (int)a.size(); }
template <typename T> void ArrayResize(std::vector<T> &a, int n) { a.resize(n); }

inline void Print() { std::cout << std::endl; }
template <typename T, typename... Rest>
void Print(const T &first, const Rest &...rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) Print(rest...);
    else std::cout << std::endl;
}

inline string DoubleToString(double v, int digits = 8) {
    char buf[64]; snprintf(buf, sizeof buf, "%.*f", digits, v); return string(buf);
}
inline string IntegerToString(long long v) { return std::to_string(v); }
inline double MathAbs(double v)  { return std::fabs(v); }
inline double MathMax(double a, double b) { return a > b ? a : b; }
inline double MathMin(double a, double b) { return a < b ? a : b; }
inline int    MathRound(double v) { return (int)std::lround(v); }

You should see: nothing — this is a header file. Confirm with ls mql5.h (Windows: dir mql5.h).

If not: no compiler? On Ubuntu or Debian sudo apt install g++; on macOS xcode-select --install; on Windows use the Windows Subsystem for Linux or MSYS2.

2
Save the build script

Go: the same folder.

Do: save this as build.sh. It compiles a .mq5 file and executes the result. Make it executable with chmod +x build.sh, or invoke it as sh build.sh.

#!/bin/sh
# build.sh NAME  ->  compiles NAME.mq5 and runs it.
# MQL5 is close enough to C++ that a C++ compiler can run the subset used here,
# with mql5.h supplying the handful of built-ins MetaTrader would provide.
# Uses whichever compiler you have.
set -e
NAME="$1"
if command -v g++ >/dev/null 2>&1 && g++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5" 2>/dev/null; then
    :
elif command -v clang++ >/dev/null 2>&1; then
    clang++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5"
else
    echo "No C++ compiler found. Install g++ (Linux: apt install g++, macOS: xcode-select --install)." >&2
    exit 1
fi
"./$NAME"

You should see: nothing — this is the toolchain. ls should show both mql5.h and build.sh.

If not: Permission denied means the chmod was skipped; use sh build.sh NAME, which needs no execute bit.

3
Count how many alerts one event produces

Go: the same folder.

Do: save this as spam.mq5 and run sh build.sh spam.

#include "mql5.h"

//+------------------------------------------------------------------+
//| The same condition, alerted three different ways.                 |
//+------------------------------------------------------------------+

// 30 ticks. The condition becomes true at tick 8 and stays true.
bool Condition(int tick) { return tick >= 8; }

datetime BarTimeOfTick(int tick) { return 1700000000 + (tick / 10) * 3600; }

void OnStart()
{
    int naive = 0;
    for(int t = 0; t < 30; t++)
        if(Condition(t)) naive++;
    Print("1. alert on every tick where the condition is true : ", naive, " alerts");

    int on_change = 0;
    bool was = false;
    for(int t = 0; t < 30; t++)
    {
        bool is = Condition(t);
        if(is && !was) on_change++;
        was = is;
    }
    Print("2. alert only when it BECOMES true                 : ", on_change, " alert");

    int per_bar = 0;
    datetime last_alerted_bar = 0;
    was = false;
    for(int t = 0; t < 30; t++)
    {
        bool is = Condition(t);
        datetime bar = BarTimeOfTick(t);
        if(is && !was && bar != last_alerted_bar)
        {
            per_bar++;
            last_alerted_bar = bar;
        }
        was = is;
    }
    Print("3. becomes true AND at most once per bar           : ", per_bar, " alert");

    Print("");
    Print("The condition was true for ", naive, " ticks and represented ONE event.");
    Print("");
    Print("Version 1 is what people write first, and it is why an indicator");
    Print("makes a phone buzz twenty-two times for a single crossover -- after");
    Print("which the alert gets switched off and is never trusted again.");
}

int main() { OnStart(); return 0; }

You should see: twenty-two alerts for one crossover, then one:

1. alert on every tick where the condition is true : 22 alerts
2. alert only when it BECOMES true                 : 1 alert
3. becomes true AND at most once per bar           : 1 alert

The condition was true for 22 ticks and represented ONE event.

Version 1 is what people write first, and it is why an indicator
makes a phone buzz twenty-two times for a single crossover -- after
which the alert gets switched off and is never trusted again.

The condition was a single event and produced 22 notifications, because OnCalculate and OnTick run on every price change and the condition stays true after it becomes true.

Version 2 is the standard fix: keep the previous state and alert only on the transition. That is the right idea and it is not sufficient on its own, which step 4 shows.

If not: if version 1 reports fewer than 22, the condition threshold moved — it becomes true at tick 8 of 30, so 22 ticks satisfy it.

4
Find out why 'alert on the transition' is not enough

Go: the same folder.

Do: save this as flicker.mq5 and run sh build.sh flicker.

#include "mql5.h"

//+------------------------------------------------------------------+
//| A condition on the forming bar does not become true once.         |
//| It becomes true, then false, then true again.                     |
//+------------------------------------------------------------------+

// One hour of ticks. The forming bar's close wobbles around the threshold.
double forming[12] = {100.4, 100.6, 100.3, 100.7, 100.5, 100.8,
                      100.2, 100.9, 100.4, 100.6, 100.5, 100.7};
const double THRESHOLD = 100.5;

void OnStart()
{
    int alerts = 0;
    bool was = false;
    Print("using the FORMING bar (index 0):");
    for(int t = 0; t < 12; t++)
    {
        bool is = forming[t] > THRESHOLD;
        if(is && !was) { alerts++; Print("   tick ", t, "  close ",
                          DoubleToString(forming[t], 2), " -> ALERT"); }
        was = is;
    }
    Print("   alerts from one bar: ", alerts);

    Print("");
    Print("using the CLOSED bar (index 1), evaluated once when the bar ends:");
    double final_close = forming[11];
    Print("   final close ", DoubleToString(final_close, 2), " > ",
          DoubleToString(THRESHOLD, 2), " -> ",
          final_close > THRESHOLD ? "ALERT (once)" : "no alert");

    Print("");
    Print("'Alert only when it becomes true' is not enough on its own. On a");
    Print("forming bar a condition becomes true repeatedly, because the value");
    Print("it is testing has not settled.");
    Print("");
    Print("Both guards are needed: evaluate on a CLOSED bar, and remember");
    Print("which bar you last alerted on.");
}

int main() { OnStart(); return 0; }

You should see: six alerts from a single bar, with the transition guard already in place:

using the FORMING bar (index 0):
   tick 1  close 100.60 -> ALERT
   tick 3  close 100.70 -> ALERT
   tick 5  close 100.80 -> ALERT
   tick 7  close 100.90 -> ALERT
   tick 9  close 100.60 -> ALERT
   tick 11  close 100.70 -> ALERT
   alerts from one bar: 6

using the CLOSED bar (index 1), evaluated once when the bar ends:
   final close 100.70 > 100.50 -> ALERT (once)

'Alert only when it becomes true' is not enough on its own. On a
forming bar a condition becomes true repeatedly, because the value
it is testing has not settled.

Both guards are needed: evaluate on a CLOSED bar, and remember
which bar you last alerted on.

The transition guard is working exactly as designed. The problem is what it is watching: on a forming bar the close is simply the latest price, so it crosses the threshold, falls back, and crosses again — and each of those is a genuine transition from false to true.

So the two guards are answering different questions and you need both. The transition guard stops an alert repeating while a condition remains true. Evaluating on a closed bar stops the condition from oscillating in the first place. Either one alone still produces a phone that buzzes six times an hour.

If not: if only one alert appears, the sample values no longer cross back below THRESHOLD between ticks — the oscillation is the effect being demonstrated, so the values must move either side of 100.5.

5
Put both guards into one function you can reuse

Go: the same folder.

Do: save this as alertfn.mq5 and run sh build.sh alertfn.

#include "mql5.h"

//+------------------------------------------------------------------+
//| One function, both guards, and the state that must survive.       |
//+------------------------------------------------------------------+

datetime g_last_alert_bar = 0;         // OUTSIDE the function -- must persist

// In MetaTrader: Alert(), SendNotification(), SendMail(), Print().
void SendAlert(string text) { Print("   >>> ALERT: ", text); }

// Returns true if an alert was actually sent.
bool AlertOncePerBar(bool condition, datetime bar_time, string text)
{
    if(!condition)                 return false;
    if(bar_time == g_last_alert_bar) return false;
    g_last_alert_bar = bar_time;
    SendAlert(text);
    return true;
}

void OnStart()
{
    // Six closed bars; the condition is true on three of them.
    datetime bars[6] = {1700000000, 1700003600, 1700007200,
                        1700010800, 1700014400, 1700018000};
    bool     cond[6] = {false, true, true, false, true, true};

    int sent = 0;
    for(int i = 0; i < 6; i++)
    {
        Print("bar ", bars[i], "  condition ", cond[i] ? "true " : "false");
        // Each bar is seen on many ticks; simulate five.
        for(int tick = 0; tick < 5; tick++)
            if(AlertOncePerBar(cond[i], bars[i], "MA crossover"))
                sent++;
    }

    Print("");
    Print("bars           : 6");
    Print("ticks per bar  : 5   (30 evaluations)");
    Print("condition true : 4 bars");
    Print("alerts sent    : ", sent);
    Print("");
    Print("Four alerts from four qualifying bars, out of thirty evaluations.");
    Print("");
    Print("g_last_alert_bar is declared outside the function on purpose. Inside");
    Print("it, it would reset on every call and every tick would alert -- the");
    Print("same scope trap that makes an EA open a trade on every tick.");
}

int main() { OnStart(); return 0; }

You should see: four alerts from thirty evaluations:

bar 1700000000  condition false
bar 1700003600  condition true 
   >>> ALERT: MA crossover
bar 1700007200  condition true 
   >>> ALERT: MA crossover
bar 1700010800  condition false
bar 1700014400  condition true 
   >>> ALERT: MA crossover
bar 1700018000  condition true 
   >>> ALERT: MA crossover

bars           : 6
ticks per bar  : 5   (30 evaluations)
condition true : 4 bars
alerts sent    : 4

Four alerts from four qualifying bars, out of thirty evaluations.

g_last_alert_bar is declared outside the function on purpose. Inside
it, it would reset on every call and every tick would alert -- the
same scope trap that makes an EA open a trade on every tick.

Four qualifying bars, four alerts, thirty evaluations. That is the behaviour a person will leave switched on.

Note where g_last_alert_bar lives. Outside the function, because it must survive between ticks — declared inside it, it would reset on every call and every tick would alert, which is the same scope trap that makes an EA open a position on every tick. The same mistake, in a third costume.

In MetaTrader, replace SendAlert with what you actually want: Alert() for a pop-up and sound, SendNotification() for the phone app, SendMail() for email, or Print() for the log. All four take a string, so the surrounding logic is unchanged.

Two practical additions worth making yourself. Include the symbol and timeframe in the message — an alert saying only “crossover” is useless when four charts are open. And remember that SendNotification is rate-limited by the terminal, so an indicator that tries to send hundreds simply stops sending, silently.

If not: if the count is 24 rather than 4, g_last_alert_bar was declared inside AlertOncePerBar — move it back to file scope, then put it back inside once to see the failure it prevents.

🎉
Check yourself before moving on

Without scrolling up: a colleague's indicator sends a phone notification when price crosses its moving average. It worked for a morning, then stopped sending anything at all, and they think the notification service is broken. What probably happened, and what would you check first? Answer: it very likely sent far too many. Without both guards the condition is evaluated on every tick against a forming bar, so a single crossover produces dozens of notifications — step 3 counted 22 for one event and step 4 counted six from a single bar even with the transition guard in place. SendNotification is rate-limited by the terminal, so once that limit is reached it stops delivering and returns false without any visible error, which looks exactly like the service being broken. What to check first is the terminal's Experts log for the send failures and the return value of the call, which almost nobody checks. Then fix the cause rather than the symptom by adding both guards: evaluate the condition on the last closed bar, and remember which bar was last alerted on so a bar can only produce one notification.

Now do it without the page: extend alertfn.mq5 so the message includes the symbol, the timeframe and the price at which the condition triggered, then add a second condition with its own independent “last alerted bar” variable. You will find one shared variable is not enough as soon as there are two signals — which is the point, and is why real indicators keep one per alert rather than one per indicator.

Best Practices

  • Always prevent duplicates — One alert per signal, not per tick
  • Include context — Symbol, timeframe, indicator value, and timestamp in every alert message
  • Make alerts optional — Use input parameters so users can enable/disable each alert type
  • Use meaningful sounds — Different sounds for buy vs sell signals help when you are away from the screen
  • Test on live charts — Alert behavior can differ between backtesting and live trading
💡
Push notifications require setup

To receive push notifications, install the MetaTrader 5 mobile app, find your MetaQuotes ID in the app settings, and enter it in MT5 Desktop under Tools > Options > Notifications. Test with SendNotification("Test") to confirm it works.