Your entry logic can be brilliant. Your risk model can be airtight. None of it matters if your order management code has a bug that skips closing a position. Or silently fails to open one. Or calculates lots wrong and sends your account to zero.
We've audited hundreds of EAs over the years. The thing that separates the ones that survive from the ones that implode? Not the strategy. The plumbing. Here are eight patterns we consider non-negotiable.
Pattern 1: Magic Number Tagging
Every EA on your MetaTrader terminal dips into the same OrdersTotal() pool. Without some way to tell whose order is whose, it's a free-for-all. Your scalper closes your swing trader's position. Your hedge gets flattened by your trend-follower.
Magic numbers solve this. Tag every order with an integer ID at creation. Three approaches we've used in production:
Static Input Magic (Simplest)
input int MagicNumber = 20251025; // Unique per EA instance
int ticket = OrderSend(Symbol(), OP_BUY, lots, Ask, 3,
sl, tp, "My EA Buy",
MagicNumber, // <-- tag the order
0, clrBlue);
// Later, when filtering:
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != MagicNumber) continue; // skip other EAs
if (OrderSymbol() != Symbol()) continue; // skip other charts
// This order belongs to us
}Always filter by both magic number AND symbol. Two instances of the same EA on EURUSD and GBPUSD share the magic number. Without the symbol check, they'll step on each other's trades.
Runtime-Switching Magic (For Grouped Orders)
Some strategies need order groups – tag the market entry with one magic, the hedge with another. Then you can close one group without touching the other:
int MagicGroup;
void OnTick()
{
if (OrdersTotal() == 0)
{
MagicGroup = 1;
PlaceBuyMarket(); // Tagged with magic 1
PlaceBuyLimit(); // Tagged with magic 1
MagicGroup = 2;
PlaceBuyLimit2(); // Tagged with magic 2
PlaceSellStop(); // Tagged with magic 2
}
}Close group 1 while keeping group 2 alive. Or vice versa. The magic number is the leash.
Offset-Based Magic (For Multi-Strategy EAs)
One EA, three strategies, zero interference:
input int MagicBase = 6614;
// Strategy A uses magic 6614
OrderSend(..., MagicBase + 0, ...);
// Strategy B uses magic 6615
OrderSend(..., MagicBase + 1, ...);
// Strategy C uses magic 6616
OrderSend(..., MagicBase + 2, ...);Each strategy manages its own orders without knowing the others exist.
Pattern 2: The Backward-Loop Close
If you only learn one pattern from this entire article, make it this one. We're not exaggerating when we say we've seen this bug in most of the EAs we've audited.
The Bug Everyone Hits
// WRONG: forward loop
for (int i = 0; i < OrdersTotal(); i++)
{
OrderSelect(i, SELECT_BY_POS, MODE_TRADES);
if (OrderType() == OP_BUY)
OrderClose(OrderTicket(), OrderLots(),
MarketInfo(OrderSymbol(), MODE_BID), 5, Red);
}What happens: Close order at index 0. The order that was at index 1 slides down to index 0. Loop increments to i=1. You just skipped an order. Four positions, two closed, two still dangling. Surprise.
The Fix: Go Backwards
// CORRECT: backward loop
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderSymbol() != Symbol()) continue;
switch (OrderType())
{
case OP_BUY:
OrderClose(OrderTicket(), OrderLots(),
MarketInfo(OrderSymbol(), MODE_BID), 5, Red);
break;
case OP_SELL:
OrderClose(OrderTicket(), OrderLots(),
MarketInfo(OrderSymbol(), MODE_ASK), 5, Red);
break;
case OP_BUYLIMIT:
case OP_BUYSTOP:
case OP_SELLLIMIT:
case OP_SELLSTOP:
OrderDelete(OrderTicket());
break;
}
}Close index 4, indices 0–3 stay put. Decrement to 3. Everything's where you expect it. Simple, elegant, correct.
The switch handles all six order types – market orders go through OrderClose() (with the correct bid/ask), pendings go through OrderDelete().
Python Equivalent
def close_all_positions(self, exchange, magic_tag=None):
"""Close all open positions, newest first."""
positions = exchange.fetch_positions()
# Sort by timestamp descending (equivalent to backward loop)
positions.sort(key=lambda p: p['timestamp'], reverse=True)
for pos in positions:
if magic_tag and pos.get('info', {}).get('clientId') != magic_tag:
continue
side = 'sell' if pos['side'] == 'long' else 'buy'
exchange.create_market_order(
pos['symbol'], side, abs(pos['contracts']))Pattern 3: Lot Splitting
Most brokers cap how much you can send per order. When your calculated lots exceed the broker's max, you split into multiple orders. This matters more than you'd think – a $500K HFT account with a 2× multiplier can easily need 760 lots. That's eight orders of 100.
#define MAX_LOT_PER_ORDER 100.0 // Broker's per-order maximum
void PlaceBuy(double totalLots)
{
double remaining = totalLots;
double lotsSent = 0;
while (remaining > 0)
{
double thisLot;
if (remaining > MAX_LOT_PER_ORDER)
{
thisLot = MAX_LOT_PER_ORDER;
remaining -= MAX_LOT_PER_ORDER;
}
else
{
thisLot = remaining;
remaining = 0;
}
int ticket = OrderSend(Symbol(), OP_BUY, thisLot,
Ask, 3, 0, 0, NULL, MagicNumber,
0, clrBlue);
if (ticket > 0)
lotsSent += thisLot;
else
Print("Lot split order failed. Error: ", GetLastError());
}
Print("Total lots deployed: ", lotsSent, " across multiple orders");
}When you need this: HFT strategies on large accounts where AccountBalance() * multiplier / divisor exceeds broker limits. A $500K account with a 2× multiplier might need 760 lots – split into 8 orders of 100 lots each.
Pattern 4: The Triple Accumulator
Counting orders isn't enough. In production, we need three things in a single pass: order count, total lot exposure, and net P&L – including commissions and swap. Skip the commission/swap part and your equity calculations will be lying to you.
We see this mistake constantly: an EA checks OrderProfit() and declares the position up $50. Meanwhile, $30 in commissions and -$5 in swap fees bring the real number to $15. Live performance "mysteriously" underperforms the backtest.
double totalLots = 0;
double totalProfit = 0;
int orderCount = 0;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderType() != OP_BUY) continue;
totalProfit += OrderProfit() + OrderCommission() + OrderSwap();
totalLots += OrderLots();
orderCount++;
}Always add OrderCommission() + OrderSwap(). Always. This is not optional.
Python Equivalent
def get_position_summary(self, positions, side='long'):
"""Triple accumulator: count, lots, net P&L."""
count = 0
total_size = 0.0
total_pnl = 0.0
for pos in positions:
if pos['side'] != side:
continue
count += 1
total_size += abs(pos['contracts'])
total_pnl += (pos.get('unrealizedPnl', 0)
- pos.get('fee', 0))
return {
'count': count,
'total_size': total_size,
'net_pnl': total_pnl
}Pattern 5: Trailing Stop Variants
Three approaches we've used, from simple to borderline obsessive:
A. Standard OrderModify Trailing
The workhorse. Move the stop behind price as profit grows. Two rules that cannot be broken: for buys, the stop only moves up. For sells, only down. Violate this and your trailing stop becomes a trailing target.
void ManageTrailingStops()
{
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderSymbol() != Symbol()) continue;
// Only trail after minimum profit threshold
double profitPoints;
if (OrderType() == OP_BUY)
profitPoints = (Bid - OrderOpenPrice()) / Point;
else if (OrderType() == OP_SELL)
profitPoints = (OrderOpenPrice() - Ask) / Point;
else continue;
if (profitPoints <= TrailingStartPoints) continue;
// Calculate new stop
double newSl;
if (OrderType() == OP_BUY)
{
newSl = NormalizeDouble(Bid - TrailingStepPoints * Point, Digits);
if (newSl > OrderStopLoss()) // Only move stop UP for buys
OrderModify(OrderTicket(), OrderOpenPrice(),
newSl, OrderTakeProfit(), 0, clrBlue);
}
else
{
newSl = NormalizeDouble(Ask + TrailingStepPoints * Point, Digits);
if (newSl < OrderStopLoss() || OrderStopLoss() == 0)
OrderModify(OrderTicket(), OrderOpenPrice(),
newSl, OrderTakeProfit(), 0, clrRed);
}
}
}Two important details: the TrailingStartPoints threshold gives the trade room to breathe before you start chasing it with a stop. And the OrderStopLoss() == 0 check handles orders that came in without an initial SL.
B. Delete-and-Replace Trailing
Sometimes you can't OrderModify() a pending order's price. The workaround: delete it and place a fresh one.
void TrailPendingOrders()
{
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
// Trail buy limits upward when price moves up
if (OrderType() == OP_BUYLIMIT && Close[0] > OrderOpenPrice())
{
OrderDelete(OrderTicket());
// Place new buy limit at current distance below price
double newPrice = Close[0] - LimitDistance * Point;
OrderSend(Symbol(), OP_BUYLIMIT, OrderLots(),
newPrice, 3, 0, 0, NULL, MagicNumber, 0, clrBlue);
}
}
}C. Multi-Breakpoint Trailing
The fancy version. Different trail distances at different profit levels. Loose when you're barely in profit. Tight when you're deep in the green.
class MultiBreakpointTrail:
"""
Trail at different distances based on profit tiers.
Config: [(profit_pips, trail_distance), ...]
Example: [(20, 15), (40, 10), (60, 5)]
- After 20 pips profit: trail 15 pips behind
- After 40 pips profit: trail 10 pips behind
- After 60 pips profit: trail 5 pips behind (tightest)
"""
def __init__(self, breakpoints):
# Sort descending by profit threshold
self.breakpoints = sorted(breakpoints, reverse=True)
def calculate_stop(self, entry_price, current_price, side='long'):
profit_pips = abs(current_price - entry_price) * 10000
for threshold, distance in self.breakpoints:
if profit_pips >= threshold:
if side == 'long':
return current_price - (distance / 10000)
else:
return current_price + (distance / 10000)
return None # No trailing yetPattern 6: Equity-Based Exit (No SL/TP)
Basket strategies are weird. You deliberately send orders with SL=0, TP=0 and manage everything through total account equity. Why? Because in a basket of 8 correlated pairs, individual pair P&L is noise. Pair #3 might be bleeding $200 while the other seven are collectively up $350. Kill pair #3's stop and you destroy the hedge.
void OnTick()
{
double balance = AccountBalance();
double equity = AccountEquity();
// Combined profit target across ALL orders
if (equity > balance + ProfitTarget)
{
CloseAllBuy();
CloseAllSell();
Print("Combined basket profit target hit");
}
// Combined loss limit across ALL orders
if (equity < balance - MaxLoss)
{
CloseAllBuy();
CloseAllSell();
Print("Combined basket loss limit hit");
}
}The basket lives or dies as a unit. Not piecemeal.
Pattern 7: OrderSend Error Handling
This one makes us genuinely angry. Most EAs we audit have zero error handling on OrderSend(). The function returns -1, and the EA barrels ahead assuming the order exists. Then wonders why trades are "missing."
The Wrong Way (Depressingly Common)
// Captures ticket but never checks it
int status3 = OrderSend(Symbol(), OP_BUY, LOT, Ask, 0, 0, 0,
NULL, 1, 0, clrBlue);
// Proceeds to next logic assuming the order existsThe Right Way
Check the ticket. Log the error. Know what went wrong.
int ticket = OrderSend(Symbol(), OP_BUY,
NormalizeDouble(lots, 2), // Always normalize
Ask,
Slippage, // Never use 0
sl, tp,
"EA Buy",
MagicNumber,
0, clrBlue);
if (ticket > 0)
{
Print("Order opened: ticket=", ticket,
" lots=", lots,
" price=", Ask);
}
else
{
int err = GetLastError();
Print("OrderSend FAILED. Error ", err, ": ", ErrorDescription(err));
// Common errors and recovery:
// 130 = Invalid stops (SL/TP too close to price)
// 131 = Invalid trade volume
// 134 = Not enough money
// 138 = Requote
// 146 = Trade context busy (retry after Sleep)
}Pre-Trade Validation
Don't wait for the broker to reject you. Check before you ask.
bool CanOpenNewTrade(double lots)
{
if (AccountFreeMargin() < MinimumFreeMargin)
{
Print("Insufficient free margin: ", AccountFreeMargin());
return false;
}
if (lots < MarketInfo(Symbol(), MODE_MINLOT))
{
Print("Lots ", lots, " below min ", MarketInfo(Symbol(), MODE_MINLOT));
return false;
}
if (lots > MarketInfo(Symbol(), MODE_MAXLOT))
{
Print("Lots ", lots, " above max ", MarketInfo(Symbol(), MODE_MAXLOT));
return false;
}
return true;
}It's cheaper to check MODE_MINLOT and free margin before the network round-trip than to deal with the rejection after.
Pattern 8: Selective Order Closing
Sometimes you need surgical precision. Not "close everything" but "close only the worst-performing buy" or "close only the order with the highest entry price." Two-pass approach: find first, close second.
void CloseHighestBuy()
{
double highestPrice = 0;
int targetTicket = 0;
double targetLots = 0;
// Pass 1: Find the buy with the highest open price
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderType() != OP_BUY) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderOpenPrice() > highestPrice)
{
highestPrice = OrderOpenPrice();
targetTicket = OrderTicket();
targetLots = OrderLots();
}
}
// Pass 2: Close just that one order
if (targetTicket > 0)
OrderClose(targetTicket, targetLots,
MarketInfo(Symbol(), MODE_BID), 5, Red);
}Find the target in pass one. Close it in pass two. Safe, predictable, no surprises. Pair this with mirror functions – CloseLowestBuy, CloseHighestSell, CloseLowestSell – and you can peel orders off a basket one at a time.
The Checklist
| Pattern | Purpose | Bug It Prevents |
|---|---|---|
| Magic number filtering | Isolate EA's orders | Cross-EA interference |
| Backward loop | Safe order deletion | Skipped orders from index shifting |
| Lot splitting | Broker limit compliance | Rejected large orders |
| Triple accumulator | Full position awareness | P&L miscalculation (missing commissions) |
| Trailing stop (with threshold) | Lock in profits | Premature stop movement |
| Equity-based exit | Basket management | Breaking hedge geometry |
| OrderSend validation | Reliable execution | Silent order failures |
| Two-pass selective close | Surgical position management | Wrong order closed |
Get Your EA Audited
If your bot is missing any of these – especially the backward loop and error handling – there are bugs hiding in there. It's not a matter of if they bite you. It's when.
We tear apart existing EAs, find the order management holes, and rebuild to production grade.


