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:
- Ship one
.mq5file. Standard library headers (#include <Trade\Trade.mqh>) are fine. - Declare one integer input for the magic number, named
InpMagic,MagicorMagicNumber. Use it inSetExpertMagicNumber()and to pick out your own positions and orders. - Trade the chart's symbol:
_Symboland_Period. - Rebuild your state from open positions in
OnInit(). The terminal restarts often. - Return
INIT_FAILED(with aPrintsaying why) when the EA can't run. - Raise
ATLAS_ALERTlines for things a person should see. - Use a hedging account if more than one EA trades the same symbol.
Don't:
- Hardcode the magic number,
#defineit, give each order its own magic (base + level), or declare several inputs with "magic" in the name. #importa DLL, callWebRequest, load a custom indicator withiCustom, or#includeyour own.mqhfiles. None of them work on an Atlaas terminal (see Upload and compile).Printpasswords, API keys or anything private. Every line lands in the terminal's logs.Printon every tick. Log decisions, not prices.- Use
Alert()orSendNotification()for alerts. They don't reach your Atlaas channels. - Base sessions on
TimeLocal()orTimeGMT()if you backtest. The tester fakes both.
Upload and compile
.mq5 source | .ex5 binary | |
|---|---|---|
| Deployable | After a build runner compiles it with MetaEditor (the compile log is on the version) | Immediately |
| Inputs form | Built from your input lines | None: you paste a .set |
| Edit in the portal | Edit code saves a new version | No |
| Size limit | 4 MB | 16 MB |
- Atlaas compiles only the file you upload. The MQL5 standard library is there
(
<Trade\Trade.mqh>,<Arrays\ArrayObj.mqh>and the rest); your own.mqhfiles are not, so paste their code into the.mq5. A compile that runs past 2 minutes fails. - Atlaas copies only the EA's
.ex5to the terminal. A custom indicator called throughiCustomisn't there; compute it inside the EA or use the built-in ones (iMA,iRSI,iATR, ...). - EAs on Atlaas charts can't import DLLs (MT5 refuses a DLL-importing EA restored from a chart
file) and can't call
WebRequest(the terminal has no allowed URLs). - Version labels are letters, digits, dots, dashes and underscores, up to 40 characters. A version can't be deleted while it is deployed.
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
- Sections:
input group "Entry"starts a section in the form. - Labels come from the comment at the end of the line; without one the form shows the name.
sinputinputs are fixed in optimisation runs.- Enums become dropdowns when the
enumis declared in the same file, or is one of MT5'sENUM_TIMEFRAMES,ENUM_APPLIED_PRICE,ENUM_MA_METHOD,ENUM_STO_PRICE,ENUM_DAY_OF_WEEK,ENUM_POSITION_TYPE,ENUM_ORDER_TYPEorENUM_ORDER_TYPE_TIME. Other enums get a text box. - Defaults must be literals. A macro or expression is shown as written, not evaluated.
- Inputs inside
#ifdefblocks are shown with a note, since Atlaas doesn't run the preprocessor. - A declaration split over several lines is skipped.
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:
- It is an integer input (
int,long,uint,ulong, ...) whose name contains "magic", in any case. - If there are several, the plain one wins:
Magic,InpMagic,Inp_Magic,ExtMagic,MagicNumber,MagicNoorMagicId.InpMagicandInpMagicSteptogether is fine.BuyMagicandSellMagictogether is not. - For an
.ex5there is no source to read, so Atlaas looks for it in the.set. Include the line (InpMagic=0is enough).
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;
}
- Use the same magic on every order, grid levels and hedges included. Atlaas matches the exact number.
- Don't use
0. MT5 uses it for trades placed by hand. - Two deployments of the same EA on one terminal get different magics, so each manages only its own positions.
- Removing a deployment and deploying again gives a new number, and the new EA won't touch positions opened under the old one. Close them first, or manage them by hand.
- To see a deployment's trades on the Strategies page, create a strategy with its magic number (shown on the deployment).
Symbols, accounts and timeframes
- A deployment is one chart: a symbol, exactly as your broker names it (
XAUUSD.m,EURUSD.r), and a timeframe:M1,M5,M15,M30,H1,H4,D1,W1orMN1. Read them from_Symboland_Periodrather than hardcoding them. If you keep a symbol input, make blank mean the chart's symbol. - On a netting account MT5 keeps one position per symbol, so two EAs on the same symbol share it and their trades can't be told apart. Use a hedging account to run more than one.
- Brokers accept different fill modes.
trade.SetTypeFillingBySymbol(_Symbol)picks one the symbol allows. - Atlaas deploys to whichever terminal you choose, live accounts included. A guard input that refuses a real account unless switched on is cheap insurance:
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:
- In
OnInit(), rebuild what you need from the open positions and orders that carry your magic, and from the deal history (HistorySelect). Don't keep the only copy of anything important in global variables. - Don't act twice on the same signal. If you trade on a new bar, start by remembering the current bar so a restart doesn't replay the last one.
- Disabling or removing a deployment does not close its positions.
- If
OnInit()fails, or MT5 can't load the EA, the deployment turns to Error with MT5's message as the reason.
Logs, alerts and notifications
- Every line your EA prints (
Print,PrintFormat) appears on the terminal's Logs page under Experts. Lines are cut at 512 characters and IP addresses are blanked. - Lines starting with
ATLAS_ALERTalso become EA alerts (Pro plan and above). The format and a helper are in Send an alert from your EA.Alert()andSendNotification()don't create them. - An alert reaches a deployment's channels when that deployment is the only one on its terminal with that symbol and timeframe. Otherwise it goes to the terminal's channels only.
- Trades (
trade.open,trade.close) come from the deal stream and are matched to the deployment by magic. A close by the broker's stop loss or take profit is titled "Stop loss hit" or "Take profit hit", so set real SL/TP levels on the position if you want those. A stop out is sent as a Failure. - Atlaas sees an EA's deals, not its rejected orders. When a trade call fails, raise an
ATLAS_ALERTatERROR(the starter EA does) so someone hears about it.
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:
TimeLocal()andTimeGMT()both return the simulated server time. For sessions in your own timezone, work fromTimeCurrent()plus an input for the server's UTC offset.MQLInfoInteger(MQL_TESTER)is true. Use it to skip anything that only makes sense live, like the real-account guard above.ATLAS_ALERTlines in a backtest are not sent anywhere.
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");
}