XAUUSD EA Orders: Filling Modes, 10030 and Stops
Most gold EAs fail on the order request, not the entry rule: filling modes that return 10030, stops-level rejections, freeze zones, requote loops, and the minimum lot that silently doubles your risk on a $52 stop.
Most gold EAs are reviewed on their entry logic and fail somewhere else: the order request. Between the signal and the fill sits a trade server with rules of its own — a filling mode it does not support, a minimum stop distance, a freeze zone around price, a volume step that rounds your risk into something you never intended. None of those failures appear in the Strategy Tester, which is why they land on the first live session and read as "the strategy stopped working".
This is the request side of the EA board. The sizing and regime framework decides how large the position should be; this piece is about whether the platform will accept it. The round trip's arithmetic is in the cost stack piece.
1. OrderSend returns a boolean; the answer is in the retcode
OrderSend() gives you true or false. The information sits in MqlTradeResult.retcode, and the codes fall into two groups needing opposite handling.
- Retryable —
TRADE_RETCODE_REQUOTE(10004),PRICE_CHANGED(10020),PRICE_OFF(10021),TOO_MANY_REQUESTS(10024). Resend with a fresh price, a bounded attempt count and a widening tolerance, then log and give up. - Terminal for that request —
INVALID_STOPS(10016),INVALID_FILL(10030),INVALID_VOLUME(10014),MARKET_CLOSED(10018),TRADE_DISABLED(10017),NO_MONEY(10019),FROZEN(10029). Repeating the identical request changes nothing; each needs a code or plan change.
A blanket retry loop is how one rejected order becomes forty requests and a session throttle. Enumerate what you handle, log what you do not, and never retry a terminal code. Code against the trade server return codes table, not the journal text.
2. Filling mode is negotiated, not chosen
SYMBOL_FILLING_MODE is a bitmask of what the symbol accepts: SYMBOL_FILLING_FOK (1), SYMBOL_FILLING_IOC (2), SYMBOL_FILLING_BOC (4), per the market info constants. A market-execution gold symbol typically advertises Fill or Kill and/or Immediate or Cancel; book-or-cancel belongs to exchange-style symbols.
Send ORDER_FILLING_IOC to a symbol that only offers FOK and the answer is 10030, invalid order filling type: no partial fill, no slippage, just a rejection. The fix is six lines:
bool PickFilling(const string sym, ENUM_ORDER_TYPE_FILLING &fill)
{
long mask = SymbolInfoInteger(sym, SYMBOL_FILLING_MODE);
if((mask & SYMBOL_FILLING_FOK) != 0) { fill = ORDER_FILLING_FOK; return true; }
if((mask & SYMBOL_FILLING_IOC) != 0) { fill = ORDER_FILLING_IOC; return true; }
return false; // nothing this EA can use: refuse to trade, do not guess
}Call it once in OnInit(), cache the result, fail loudly on false. Guessing ORDER_FILLING_RETURN as a fallback is worse than refusing: that mode exists for exchange execution and turns a partial fill into a resting remainder you never planned.
3. Stops level and freeze level are distances the server owns
SYMBOL_TRADE_STOPS_LEVEL is the minimum distance, in points, between price and any stop or limit. SYMBOL_TRADE_FREEZE_LEVEL is the distance inside which the server refuses to modify or cancel orders and positions — TRADE_RETCODE_FROZEN (10029), not an error worth retrying.
Both are quoted in points, so both must pass through SYMBOL_POINT before being compared to a price. Thirty points of minimum stop is $0.30 on a two-decimal gold feed and $0.03 on a three-decimal one: identical number, different money. That conversion is where a stop that backtested cleanly becomes one the server rejects.
double ClampStop(const string sym, double price, double stop, int dir)
{
long lvl = SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
double min = (double)lvl * point;
if(min <= 0.0) return stop;
if(dir > 0 && price - stop < min) return price - min; // push a buy stop away
if(dir < 0 && stop - price < min) return price + min; // push a sell stop away
return stop;
}Clamping changes the trade: a stop pushed out is wider than the R multiple assumed. Recompute size after the clamp, or reject the setup when it moves the stop by more than a fifth. Silently widening is how a 1R loss becomes 1.6R on the ledger.
4. The tester reproduces none of this
The Strategy Tester fills your order. It does not requote you, refuse an unsupported filling mode, or enforce a freeze level, and spread and slippage are assumptions you supply. A system can run four years of real ticks without one 10030 and fail every entry live.
The honest test is a demo or small live run logging retcode on every request for a week. Two numbers matter: rejection rate and retry depth. Above a few percent on entries the request is wrong, not the market; a retry depth above two is a spread or latency problem that belongs in execution cost.
5. The minimum lot, not your formula, sets your risk
Take today's gold. The 14 completed sessions to 3 November 2026 average a $75.20 true range, so a 0.7xATR stop is $52.64 — and on one lot of 100 ounces that stop is $5,264 of risk. The arithmetic that follows is not opinion: 0.5% of a $5,000 account is $25, which is 0.0047 lots. There is no such order. The smallest executable position is 0.01 lots, risking $52.64 — 1.05% of the account, more than double the intent. Running 0.5% at minimum size needs $10,528 of equity.
That is the sizing gate most gold EAs get wrong: the formula divides correctly and the volume step rounds it into a different trade.
double MinLotRisk(const string sym, double stopDistance)
{
double tv = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
double ts = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
double mn = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
if(ts <= 0.0) return 0.0;
return (stopDistance / ts) * tv * mn;
}If MinLotRisk() exceeds the per-trade budget, the action is no trade — not a smaller stop. Shrinking the stop to fit the lot turns a noise-band exit into a guaranteed loss, the same failure the prop-firm guard catches from the other end. Volatility makes it worse: those fourteen sessions ran from a $37.90 range to $156.96, so one lot carries four times the risk depending on the week.
6. The symbol name that stops an EA silently
A hardcoded "XAUUSD" works until the broker suffixes it — XAUUSD.a, XAUUSDm, GOLD — and then SymbolInfoDouble() returns zero, every tick is skipped on a tickSize <= 0 guard, and the EA runs for a month without placing an order. Nothing in the journal says "wrong symbol"; the chart looks alive and the account does not move.
Resolve it at start-up: try the configured string, scan SymbolsTotal(false) and SymbolName(i, false) for a case-insensitive prefix match, SymbolSelect() the winner and log it. If nothing matches, print an explicit error and refuse to initialise rather than running empty.
7. The request checklist
- Cache
SYMBOL_FILLING_MODEat init, pick FOK or IOC, refuse if neither is offered. - Convert stops level and freeze level through
SYMBOL_POINT; never compare points to dollars. - Recompute size after any stop clamp; reject if it moves the stop over 20%.
- Split retcodes into retryable and terminal; cap attempts at two.
- Gate on
MinLotRisk(): if minimum size breaks the budget, stand down. - Verify after entry — a
POSITION_SLof zero is a naked position, not a filled plan. - Log
retcode, requested and filled price, and the resolved symbol on every send (OrderSend, OrderCheck).
OrderCheck is worth running first — it validates volume and margin client-side — but it is a pre-check, not an acceptance. The server still decides, so every retcode path stays live in the code.
Sources: MQL5 trade server return codes · Symbol trade constants · OrderSend · OrderCheck · gold daily bars from Investing.com XAU/USD, 1 September to 3 November 2026, desk arithmetic.
This is engineering documentation, not financial advice. Backtests are not live results: a tester report cannot show whether a broker will accept your order, at what fill, or with the stops attached.