DEV Community

Omais Ali
Omais Ali

Posted on

Sending push notifications from an MQL5 EA without getting them switched off

A push notification to your phone is the simplest way to know what an Expert Advisor is doing while you are away from the terminal. MQL5 has one function for it, SendNotification(), and most EAs call it directly wherever something interesting happens.

That works until the day something goes wrong. A disconnect, a loop of rejected orders or a burst of partial fills makes the EA call SendNotification() many times in a few seconds. The terminal has strict limits on this function, and if they are broken it can disable notifications for the program. So the one moment you most need the alert is the moment it stops arriving.

This tutorial builds a small notifier that respects the limits, skips duplicates and still logs everything.

The rules from the documentation

  1. The message can be at most 255 characters.
  2. No more than 2 calls per second and no more than 10 calls per minute. The terminal monitors this dynamically and can disable the function when the limit is broken.
  3. SendNotification() does not work in the Strategy Tester.
  4. Notifications must be enabled in the terminal, with your MetaQuotes ID entered under Tools, Options, Notifications. You can check this from code with TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED).
  5. On failure, GetLastError() returns one of 4515 (ERR_NOTIFICATION_SEND_FAILED), 4516 (ERR_NOTIFICATION_WRONG_PARAMETER), 4517 (ERR_NOTIFICATION_WRONG_SETTINGS) or 4518 (ERR_NOTIFICATION_TOO_FREQUENT).

The design follows from those rules: never send from the place where the event happens. Put the message in a queue and send from a timer at a pace the terminal accepts.

Queueing messages

#define NOTIFY_MAX_PER_MIN 8     // stay below the limit of 10
#define NOTIFY_MAX_QUEUE   50

string   gQueue[];
ulong    gSent[];                // GetTickCount64() of recent sends
string   gLastText     = "";
datetime gLastTextTime = 0;

bool NotifyAvailable()
{
   if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
      return false;
   return (bool)TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED);
}

void Notify(const string text, const int dedupeSeconds = 300)
{
   Print("NOTIFY: ", text);      // the log always gets the message

   if(!NotifyAvailable())
      return;

   // the same text again within a few minutes adds nothing
   if(text == gLastText && TimeLocal() - gLastTextTime < dedupeSeconds)
      return;
   gLastText     = text;
   gLastTextTime = TimeLocal();

   string msg = StringFormat("%s %I64d: %s", _Symbol,
                             AccountInfoInteger(ACCOUNT_LOGIN), text);
   if(StringLen(msg) > 255)
      msg = StringSubstr(msg, 0, 255);

   int n = ArraySize(gQueue);
   if(n >= NOTIFY_MAX_QUEUE)
   {
      ArrayRemove(gQueue, 0, 1); // drop the oldest, keep the newest
      n--;
   }
   ArrayResize(gQueue, n + 1);
   gQueue[n] = msg;
}
Enter fullscreen mode Exit fullscreen mode

A few choices here are deliberate.

The symbol and account number go in front of every message. If you run several EAs or several accounts, a bare "Daily loss limit reached" on a phone screen is not enough to act on.

Duplicates are dropped by text. An EA that fails to modify a stop on every tick would otherwise produce the same alert hundreds of times. Use TimeLocal() for this check rather than TimeCurrent(), because server time stops moving when no quotes arrive, for example over the weekend.

The queue has a cap. If the terminal cannot send for a long time, you do not want a backlog of stale alerts arriving an hour later.

Sending at a safe pace

void FlushNotifications()
{
   if(ArraySize(gQueue) == 0)
      return;

   // forget sends older than 60 seconds
   ulong now  = GetTickCount64();
   int   keep = 0;
   for(int i = 0; i < ArraySize(gSent); i++)
      if(now - gSent[i] < 60000)
         gSent[keep++] = gSent[i];
   ArrayResize(gSent, keep);

   if(keep >= NOTIFY_MAX_PER_MIN)
      return;                    // wait until the window moves on

   ResetLastError();
   if(SendNotification(gQueue[0]))
   {
      ArrayResize(gSent, keep + 1);
      gSent[keep] = now;
      ArrayRemove(gQueue, 0, 1);
      return;
   }

   int err = GetLastError();
   PrintFormat("SendNotification failed, error %d", err);

   // these will never succeed, so do not retry them forever
   if(err == ERR_NOTIFICATION_WRONG_PARAMETER ||
      err == ERR_NOTIFICATION_WRONG_SETTINGS)
      ArrayRemove(gQueue, 0, 1);
}
Enter fullscreen mode Exit fullscreen mode

Call it from a one second timer. That alone keeps you at no more than one message per second, and the rolling window keeps you under the per minute limit with some room to spare.

int OnInit()
{
   EventSetTimer(1);
   if(!NotifyAvailable())
      Print("Push notifications are off or not configured, alerts go to the log only");
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   EventKillTimer();
}

void OnTimer()
{
   FlushNotifications();
}
Enter fullscreen mode Exit fullscreen mode

If your EA already uses a timer for other work, call FlushNotifications() at the end of the existing OnTimer() instead of creating a second one. An EA can only have one timer.

What is worth a notification

The limit of 10 per minute is a good reminder that a phone alert should mean "look at this". Some events that usually earn one:

  1. A position opened or closed, with volume, price and the reason. OnTradeTransaction() with TRADE_TRANSACTION_DEAL_ADD is the reliable place to catch this.
  2. A trade request rejected with a retcode the EA cannot handle on its own.
  3. A risk rule triggered, such as a daily loss limit or a maximum drawdown lock.
  4. Lost connection that lasts longer than a minute. Check TerminalInfoInteger(TERMINAL_CONNECTED) in the timer and only alert when the state has been bad for a while, then send one more message when it recovers.
  5. The EA being removed or the terminal closing. In OnDeinit() the timer is already gone, so call SendNotification() directly there, once, with the reason code.

Everything else belongs in the log. Normal events like a new bar or a trailing stop step will drown out the alerts that matter.

Testing it

SendNotification() does nothing in the Strategy Tester, so test on a demo account. A quick check is a script or a temporary input that queues 30 messages at once. You should see them arrive on the phone at a steady pace over a few minutes, and no 4518 errors in the Experts log. Then send the same text 30 times and confirm only one arrives.

A notifier that slows down under pressure is far more useful than one that goes silent exactly when the EA is in trouble.

Top comments (0)