Expert Advisors

Atlaas runs your own MQL5 Expert Advisors (EAs). You upload a version, deploy it to a chart on one of your terminals, and Atlaas starts the terminal with it. Its trades, alerts and failures then reach your notification channels, the Strategies page and the live-vs-backtest monitor. Uploads and deployments live in the portal under Expert advisors; the same operations are available through the API, the SDK and the MCP tools.

This section is how to write an EA that works with all of that. A starter EA at the end follows every rule here.

Checklist

Do:

Don't:

Upload and compile

.mq5 source.ex5 binary
DeployableAfter a build runner compiles it with MetaEditor (the compile log is on the version)Immediately
Inputs formBuilt from your input linesNone: you paste a .set
Edit in the portalEdit code saves a new versionNo
Size limit4 MB16 MB

Inputs

For an .mq5, Atlaas reads your input lines and builds the inputs form from them. The parser reads one declaration per line, the way MT5's own Inputs tab does:

input group "Entry"
input int    InpFastLen = 9;                     // Fast EMA
input double InpLots    = 0.10;                  // Lots per position
input ENUM_TIMEFRAMES InpTrendTf = PERIOD_H1;    // Trend timeframe
sinput bool  InpDebug   = false;                 // Print every order

The form saves a .set: one name=value line per input. You can paste one instead (and must, for an .ex5):

InpMagic=1000
InpLots=0.01
InpUseSess=true
InpTrendTf=16385

bool is true/false and an enum is its number (16385 is PERIOD_H1). Lines starting with ; are comments, and the ||start||step||stop optimisation ranges MT5 adds are ignored on deployments. Presets are named .set files saved on the EA; apply one to a deployment or a backtest, and its values are matched by input name.

Magic numbers

MT5 stamps a magic number on every order an EA places and hands it back on every deal. It is how Atlaas knows which deployment a trade belongs to, so it drives the deployment's notifications, the live-vs-backtest monitor, the trade labels on the terminal and the Strategies page.

Atlaas owns the number. Each deployment gets its own magic, unique across your account (from 1000, skipping numbers your strategies use), and keeps it for life. Every time the terminal starts, Atlaas writes that number into your EA's magic input, whatever the .set says. The inputs form shows it read-only.

How Atlaas finds the input:

When Atlaas can't find exactly one, the version shows no magic input and the EA keeps whatever magic it uses. Its trades then aren't linked to the deployment: no deployment notifications and no live-vs-backtest comparison.

In the code:

input long InpMagic = 1000;   // Magic number (Atlaas sets this)

CTrade trade;

int OnInit()
{
   trade.SetExpertMagicNumber((ulong)InpMagic);   // every order carries it
   return INIT_SUCCEEDED;
}

// Only the positions this deployment opened.
bool IsOurs()
{
   return PositionGetString(POSITION_SYMBOL) == _Symbol
       && PositionGetInteger(POSITION_MAGIC) == InpMagic;
}

Symbols, accounts and timeframes

input bool InpAllowReal = false;   // Allow a real-money account

int OnInit()
{
   if(!InpAllowReal && !MQLInfoInteger(MQL_TESTER)
      && AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_REAL)
   {
      Print("Real account and InpAllowReal is off: not trading");
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

Restarts and state

MT5 only loads charts when it starts, so the terminal restarts (in about a minute) whenever a deployment on it is created, enabled, disabled, edited or removed. Schedules, updates and recovery restart it too. Your EA must pick up where it left off:

Logs, alerts and notifications

See Where events go for which groups a deployment can route and on which plan.

Backtests and live vs backtest

Backtests run the same version and .set in MT5's Strategy Tester. The tester differs from a live terminal in a few ways that catch EAs out:

The live vs backtest monitor takes a deployment's live trades (by its magic), backtests the same window, and pairs trades by symbol, direction and open time within 5 minutes. It works best when entries are decided on closed bars, which the tester reproduces closely, and when the magic hasn't changed.

Starter EA

A complete EA that follows every rule above: an EMA crossover that holds at most one position.

//+------------------------------------------------------------------+
//| atlaas-starter.mq5                                               |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

input group "Atlaas"
input long   InpMagic     = 1000;    // Magic number (Atlaas sets this)
input bool   InpAllowReal = false;   // Allow a real-money account

input group "Strategy"
input int    InpFastLen   = 9;       // Fast EMA
input int    InpSlowLen   = 21;      // Slow EMA
input double InpLots      = 0.01;    // Lots per trade
input int    InpSlPoints  = 300;     // Stop loss in points (0 = off)
input int    InpTpPoints  = 600;     // Take profit in points (0 = off)

CTrade   trade;
int      fastHandle = INVALID_HANDLE;
int      slowHandle = INVALID_HANDLE;
datetime lastBar    = 0;

// One Experts-log line that Atlaas turns into an alert.
void AtlasAlert(string message, string level = "INFO")
{
   StringReplace(message, "\\", "\\\\");
   StringReplace(message, "\"", "\\\"");
   Print("ATLAS_ALERT {\"message\":\"", message, "\",\"symbol\":\"", _Symbol, "\",\"level\":\"", level, "\"}");
}

int OnInit()
{
   if(InpMagic <= 0)
   {
      Print("InpMagic must be a positive number");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(!InpAllowReal && !MQLInfoInteger(MQL_TESTER)
      && AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_REAL)
   {
      AtlasAlert("Real account and InpAllowReal is off: not trading", "ERROR");
      return INIT_FAILED;
   }

   trade.SetExpertMagicNumber((ulong)InpMagic);
   trade.SetTypeFillingBySymbol(_Symbol);

   fastHandle = iMA(_Symbol, _Period, InpFastLen, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, InpSlowLen, 0, MODE_EMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
   {
      Print("Could not create the moving averages");
      return INIT_FAILED;
   }

   // A restart must not replay the last closed bar's signal.
   lastBar = iTime(_Symbol, _Period, 0);
   PrintFormat("Started: magic %I64d, %d open position(s)", InpMagic, CountPositions());
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   IndicatorRelease(fastHandle);
   IndicatorRelease(slowHandle);
}

void OnTick()
{
   datetime bar = iTime(_Symbol, _Period, 0);
   if(bar == lastBar)
      return;
   lastBar = bar;

   // [0] is two bars back, [1] the bar that just closed.
   double fast[2], slow[2];
   if(CopyBuffer(fastHandle, 0, 1, 2, fast) != 2 || CopyBuffer(slowHandle, 0, 1, 2, slow) != 2)
      return;
   bool crossUp   = fast[0] <= slow[0] && fast[1] > slow[1];
   bool crossDown = fast[0] >= slow[0] && fast[1] < slow[1];
   if(!crossUp && !crossDown)
      return;

   ClosePositions(crossUp ? POSITION_TYPE_SELL : POSITION_TYPE_BUY);
   if(CountPositions() == 0)
      Open(crossUp ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
}

// Only the positions this deployment opened: same symbol, same magic.
bool IsOurs()
{
   return PositionGetString(POSITION_SYMBOL) == _Symbol
       && PositionGetInteger(POSITION_MAGIC) == InpMagic;
}

int CountPositions()
{
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
      if(PositionGetTicket(i) > 0 && IsOurs())
         count++;
   return count;
}

void ClosePositions(ENUM_POSITION_TYPE type)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !IsOurs() || PositionGetInteger(POSITION_TYPE) != type)
         continue;
      if(!trade.PositionClose(ticket))
         AtlasAlert(StringFormat("Close of #%I64u failed: %s", ticket, trade.ResultRetcodeDescription()), "ERROR");
   }
}

void Open(ENUM_ORDER_TYPE type)
{
   double price = type == ORDER_TYPE_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double dir   = type == ORDER_TYPE_BUY ? 1 : -1;
   double sl    = InpSlPoints > 0 ? NormalizeDouble(price - dir * InpSlPoints * _Point, _Digits) : 0;
   double tp    = InpTpPoints > 0 ? NormalizeDouble(price + dir * InpTpPoints * _Point, _Digits) : 0;
   if(!trade.PositionOpen(_Symbol, type, InpLots, price, sl, tp))
      AtlasAlert(StringFormat("%s %.2f failed: %s", EnumToString(type), InpLots, trade.ResultRetcodeDescription()), "ERROR");
}