Magic numbers in MQL5: managing multiple EAs on one account
The terminal does not police magic numbers. It stores whatever integer you attach to an order and hands it back later, with no check that another expert has already claimed it. Treat that integer as a partition key rather than a label, and running several strategies on one account stops being a fight.
What the magic actually tags
Every order you send carries a magic, set through MqlTradeRequest.magic or, more conveniently, CTrade::SetExpertMagicNumber(). When the order fills, the resulting position inherits it, and every deal written to history carries it too. That last part matters more than the first: DEAL_MAGIC is what lets you reconstruct a single strategy's P&L months later, long after the positions are closed and forgotten.
A magic of 0 usually means "not set", which in practice means you clicked the button yourself. Keep it that way — it gives manual interventions a bucket of their own.
Netting accounts break the model
Worth saying plainly. On a netting account there is one position per symbol, so two EAs trading the same pair merge into a single position carrying whichever magic opened it first. The second EA's filter will not find its own trades, and its exit logic will either do nothing or close someone else's work. Magic-based separation only holds reliably on hedging accounts, or on netting accounts where each EA sticks to its own symbols. Check the account's margin mode before building anything on this.
Rules that hold up
1. One magic per running instance, exposed as an input. Never hardcode it inside the EA body.
2. Reserve 0 for manual trades and refuse to start if the input is 0.
3. Filter by symbol *and* magic. Magic alone collides across symbols if you ever reuse a block.
4. Never loop PositionsTotal() to count, modify or close without both filters. This is the most common way one EA wrecks another.
5. Run a collision check in OnInit. At start-up you own nothing, so any live position already carrying your magic belongs to somebody else.
6. Query history by magic rather than by time window alone when reporting.
7. Retire magics instead of recycling them. Reassigning an old number to a new strategy makes the history unreadable.
A filter that compiles
The functions below are the whole trick. PositionGetTicket(i) selects a position as a side effect, so every PositionGetString / PositionGetInteger call straight after it refers to that one position.
//+------------------------------------------------------------------+
//| MagicScope.mq5 |
//| Attribute every position to one EA and touch nothing else. |
//+------------------------------------------------------------------+
#property version "1.00"
#include <Trade\Trade.mqh>
input long InpMagic = 20260101; // unique per running instance
input double InpTarget = 50.0; // close own basket at this profit
input string InpTag = "ScopeDemo";
CTrade trade;
//--- how many positions this EA owns on this symbol
int OwnCount(const string symbol, const long magic)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) != symbol)
continue;
if(PositionGetInteger(POSITION_MAGIC) != magic)
continue;
count++;
}
return count;
}
//--- floating P/L of this EA's slice, swaps included
double OwnProfit(const string symbol, const long magic)
{
double profit = 0.0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) != symbol)
continue;
if(PositionGetInteger(POSITION_MAGIC) != magic)
continue;
profit += PositionGetDouble(POSITION_PROFIT)
+ PositionGetDouble(POSITION_SWAP);
}
return profit;
}
//--- close only what we own, ticket by ticket
void CloseOwn(const string symbol, const long magic)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) != symbol)
continue;
if(PositionGetInteger(POSITION_MAGIC) != magic)
continue;
if(!trade.PositionClose(ticket))
PrintFormat("close failed ticket=%I64u ret=%d",
ticket, trade.ResultRetcode());
}
}
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
trade.SetTypeFillingBySymbol(_Symbol);
trade.SetDeviationInPoints(20);
if(InpMagic <= 0)
{
Print("Refusing to run: magic 0 is the manual-trading bucket.");
return INIT_PARAMETERS_INCORRECT;
}
//--- we own nothing yet, so any match is a collision
const int foreign = OwnCount(_Symbol, InpMagic);
if(foreign > 0)
PrintFormat("[%s] collision: %d position(s) on %s already use magic %I64d.",
InpTag, foreign, _Symbol, InpMagic);
return INIT_SUCCEEDED;
}
void OnTick()
{
//--- entries are omitted here; trade.Buy/Sell carry the magic for you
if(OwnCount(_Symbol, InpMagic) == 0)
return;
if(OwnProfit(_Symbol, InpMagic) >= InpTarget)
CloseOwn(_Symbol, InpMagic);
}
Allocate blocks, not numbers
Once you run more than two or three experts, invent a scheme and write it down. A readable one: magic = strategy * 100000 + symbolIndex * 1000 + instance. Strategy 4 on symbol 2, instance 1 gives 402001. You can decode the components later, reserve ranges for future symbols, and you never have to remember which number was free. The field holds a long, so six digits cost nothing.
Hashing strategy name plus symbol also works, but then you can only match a magic, never read it.
Attribution is the payoff
With consistent magics, per-strategy P&L is a short loop: HistorySelect(from, to), walk HistoryDealsTotal(), read DEAL_MAGIC, DEAL_PROFIT, DEAL_SWAP and DEAL_COMMISSION, and accumulate into a map keyed by magic. That gives you a breakdown the account curve cannot. A strategy can look perfectly respectable on win rate and still be the thing draining the account — [see a real backtest](https://mql.ranartech.com/r/6V4RG96MUmMI), an AUDUSD M15 EA that won 60% of its 565 trades and finished $2,517 down. Blended into a three-EA account, that would just look like a bad month.
Where people slip
- Setting the magic on the entry request but not on the close or modify request. Positions inherit from entry, so it usually does no harm, but the gap shows the moment you start parsing order history instead of positions.
- Mixing
intandlong.PositionGetInteger(POSITION_MAGIC)returns along. Compare like with like and keep the input type consistent. - Copying an EA folder for a second instance and forgetting to change the magic. Two copies then share a filter, each closing the other's positions, and neither's exit logic behaves as tested.
- Confusing orders with positions.
OrdersTotal()andPositionsTotal()are separate lists holding separate objects, and both carry magic fields.
None of this is clever. It is just discipline applied to a single integer, and it is the difference between a portfolio of strategies and a pile of experts shouting over each other.
Build & backtest your own EA free →