Building a stable Expert Advisor for XAUUSD is a different challenge than coding one for EURUSD or GBPJPY. Gold has a personality—and if you don't respect it, the market will wipe out your account faster than you can say "margin call." I've been down that road, and I've learned the hard way that generic EAs simply don't work on gold.
Why Gold Requires a Specialized Approach
The first thing to understand is that XAUUSD is not a "currency pair" in the traditional sense. It's a commodity quoted in dollars, and its behavior is shaped by a completely different set of drivers. According to the Bank for International Settlements (BIS) March 2026 quarterly review, retail investor participation in gold has surged roughly threefold over the past six months, while institutional flows have simultaneously retreated . This structural shift creates a market environment where retail-driven momentum and leveraged ETF rebalancing amplify price swings, particularly during periods of sharp reversal . The BIS explicitly noted that "leveraged ETFs' daily rebalancing mechanisms and margin-triggered forced liquidations amplified market volatility" .
What does this mean for EA design? It means we're dealing with a market that experiences violent, low-probability moves that are not necessarily justified by macroeconomic fundamentals. A grid EA that blindly adds positions at fixed intervals is a death sentence in gold—one sudden 30% move (as silver experienced in January 2026) and your entire basket gets stopped out .
My Core Design Philosophy: Volatility-Adaptive Structure
The proprietary angle I want to emphasize is this: the grid step size should be a function of recent volatility, not a fixed pip value. Most commercial gold EAs I've reviewed use a static GridStepPips parameter . This is fundamentally flawed because gold's average true range (ATR) varies dramatically across different market sessions and macroeconomic regimes. A 200-pip step might be appropriate during the quiet Asian session but completely inadequate during the London-New York overlap when volatility spikes.
This EA dynamically adjusts the grid spacing based on the ATR indicator. When volatility increases, the grid widens, reducing the frequency of recovery orders and protecting the account from being caught in a high-velocity trend. When volatility contracts, the grid tightens, allowing the EA to capture smaller oscillations. This is a risk-first approach, not a profit-maximization approach.
Trading Session Logic: Why I Filter by Time
I've also incorporated a session filter, and here's why. Gold's liquidity profile is not uniform across the 24-hour trading day. According to data from the World Gold Council, the most active trading period for gold is the overlap between London and New York sessions, roughly 13:00 to 17:00 GMT . During these hours, spreads tighten and price discovery is most efficient. Conversely, the late Asian session (approximately 23:00 to 01:00 GMT) often exhibits thin liquidity and wider spreads, making it a dangerous environment for grid entries.
The EA uses a configurable session filter to restrict new trade initiation to the user-defined hours. This doesn't mean we close existing positions outside the session—it simply means we don't open new ones. This simple filter significantly reduces the risk of being caught in low-liquidity spikes.
Strategy Overview
*Timeframe Recommendation:* M15. This is the most commonly recommended timeframe for gold EAs . It provides enough granularity to react to intraday moves without the noise of lower timeframes (like M1) and without the sluggishness of higher timeframes (like H1). All trend and volatility calculations in this EA reference the M15 data by default.
*Entry Logic:*
1. Trend Confirmation: The EA first determines the prevailing trend on a higher timeframe (H1 by default) using a 200-period Simple Moving Average (SMA). Price must be above the SMA for buy entries and below for sell entries. This prevents the EA from trading against the primary momentum.
2. Entry Trigger: A new trade is initiated only when the price retraces to the dynamically calculated ATR-based grid step from the current average price of open positions. This means the EA is not entering arbitrarily—it's waiting for a pullback before adding to the basket.
3. Session Filter: The entry must occur within the user-defined trading hours.
*Recovery Grid Logic:*
1. When price moves against the initial position by more than GridStepPips (adjusted dynamically by ATR), a recovery order is placed in the same direction.
2. The lot size of each recovery order increases by the GridMultiplier (recommended: 1.5 to 2.0) to aid in pulling the average price back toward the market.
3. The grid continues until either MaxGridOrders is reached, or the entire basket reaches the GridTargetProfit in pips from the break-even point.
4. S/R Protection: The EA calculates significant support and resistance levels on the H1 timeframe. If a pending recovery order would be placed within a configurable tolerance zone of these levels, the order is delayed. This prevents the EA from buying into resistance or selling into support—a critical safety feature that's rarely implemented in commercial EAs.
*Risk Management:*
The Code
Below is the complete MQL4 source code. I've annotated every input parameter with its purpose, data range, and default value. Compile this in MetaEditor, load it onto an XAUUSD M15 chart, and test it extensively on a demo account before going live.
```cpp
//+------------------------------------------------------------------+
//| GoldAdaptiveEA.mq4 |
//| Copyright 2026, FXEAR.com |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict
//+------------------------------------------------------------------+
//| INPUT PARAMETERS (with detailed comments) |
//+------------------------------------------------------------------+
//===== GENERAL SETTINGS =====
input int MagicNumber = 20260621; // EA identifier. Must be unique for each EA instance on the same account.
input string CustomComment = "GoldAdapt"; // Comment tag for orders. Helps identify trades opened by this EA.
input int Slippage = 30; // Maximum allowed price slippage in points (not pips). For gold, 1 point = 0.01 if 2-decimal broker.
input int MaxSpread = 80; // Maximum allowed spread in points. Prevents entry during high-volatility/news periods.
//===== TREND FILTER (Higher Timeframe) =====
input int TrendMAPeriod = 200; // Period for the SMA trend filter. Default 200 is a widely-followed institutional level.
input ENUM_TIMEFRAMES TrendTF = PERIOD_H1; // Timeframe for trend calculation. H1 provides a clear macro view.
//===== GRID & VOLATILITY (ATR-based) =====
input int ATRPeriod = 14; // Period for ATR calculation. 14 is standard for volatility measurement.
input double ATRMultiplier = 1.5; // Multiplier for ATR to calculate grid step. Higher = wider grid, lower = tighter grid.
input double GridStepPipsBase = 150.0; // Base grid step in pips (before ATR adjustment). 1 pip = 10 points for gold.
input double GridMultiplier = 1.8; // Lot size multiplier for each recovery order. 1.8 = 0.01 -> 0.018 -> 0.0324...
input int MaxGridOrders = 5; // Maximum number of recovery orders in a single grid. Prevents infinite averaging.
input double GridTargetProfit = 100.0; // Target profit for the entire grid, measured in pips from break-even.
//===== SESSION FILTER =====
input bool UseSessionFilter = true; // Enable/disable time-based trading restrictions.
input int SessionStartHour = 7; // UTC hour to start trading (0-23). Default 7 AM GMT = London open.
input int SessionEndHour = 22; // UTC hour to stop trading. Default 10 PM GMT = end of US session.
//===== RISK MANAGEMENT =====
input double RiskPercent = 0.5; // % of balance per trade. 0.5% = conservative. Range: 0.1 - 2.0.
input double MaxFloatingLossPct = 15.0; // Max floating loss % of balance. EA will close all if exceeded. Default 15%.
input bool TradeFriday = false; // Allow new trades on Friday? false = no new trades on Fridays.
input int StopLossPips = 0; // Fixed SL in pips for each order. Set to 0 to disable (use grid recovery instead).
//===== SUPPORT/RESISTANCE PROTECTION =====
input bool UseSRProtection = true; // Enable/disable S/R zone filtering for recovery orders.
input int SR_ScanBars = 200; // Number of bars to scan for S/R levels on the TrendTF.
input double SR_ZonePips = 80.0; // Zone width in pips around S/R levels. Orders delayed if inside this zone.
//+------------------------------------------------------------------+
//| GLOBAL VARIABLES |
//+------------------------------------------------------------------+
double gridStepPips;
double lotSize;
double currentATR;
double currentSL;
double currentTP;
int gridCount;
bool isGridActive;
datetime lastBarTime;
datetime lastTradeTime;
double supportLevel;
double resistanceLevel;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Validate inputs
if(RiskPercent <= 0 || RiskPercent > 5)
{
Print("Error: RiskPercent must be between 0.1 and 5.0");
return(INIT_PARAMETERS_INCORRECT);
}
if(GridMultiplier < 1.0 || GridMultiplier > 3.0)
{
Print("Error: GridMultiplier must be between 1.0 and 3.0");
return(INIT_PARAMETERS_INCORRECT);
}
// Calculate initial lot size based on risk
lotSize = NormalizeLot(AccountBalance() * RiskPercent / 100.0 / 1000.0);
if(lotSize < MarketInfo(Symbol(), MODE_MINLOT)) lotSize = MarketInfo(Symbol(), MODE_MINLOT);
gridStepPips = GridStepPipsBase;
gridCount = 0;
isGridActive = false;
lastBarTime = 0;
// Initial S/R calculation
CalculateSRLevels();
Print("GoldAdaptiveEA initialized. Lot size: ", lotSize);
Print("Risk: ", RiskPercent, "%, Grid Step: ", gridStepPips, " pips");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Clean up chart objects if needed
ObjectsDeleteAll(0, "GA_");
Print("GoldAdaptiveEA deinitialized. Reason: ", reason);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//-- 1. Check if we need to process a new bar (M15)
if(Time[0] == lastBarTime) return;
lastBarTime = Time[0];
//-- 2. Update ATR and dynamic grid step
currentATR = iATR(Symbol(), PERIOD_M15, ATRPeriod, 1);
gridStepPips = GridStepPipsBase + (currentATR / Point() * ATRMultiplier / 10.0);
if(gridStepPips < 50) gridStepPips = 50.0; // minimum step
if(gridStepPips > 500) gridStepPips = 500.0; // maximum step
//-- 3. Check Friday trade restriction
if(!TradeFriday && DayOfWeek() == 5) return;
//-- 4. Check session filter
if(UseSessionFilter)
{
int currentHour = Hour();
if(currentHour < SessionStartHour || currentHour >= SessionEndHour) return;
}
//-- 5. Check spread
if(MarketInfo(Symbol(), MODE_SPREAD) > MaxSpread) return;
//-- 6. Update S/R levels (once per bar)
if(Time[0] % 3600 == 0) CalculateSRLevels();
//-- 7. Check existing grid status
int totalOrders = CountGridOrders();
if(totalOrders == 0)
{
// No active grid - look for new entry
if(CheckEntrySignal())
{
OpenFirstOrder();
}
}
else
{
// Grid is active - check if we need to add recovery orders
if(totalOrders < MaxGridOrders)
{
CheckRecoveryEntry();
}
// Check if grid target profit is reached
if(CheckGridProfitTarget())
{
CloseAllGridOrders();
}
}
//-- 8. Floating loss protection
CheckFloatingLossProtection();
}
//+------------------------------------------------------------------+
//| Count open orders belonging to this EA |
//+------------------------------------------------------------------+
int CountGridOrders()
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
count++;
}
}
}
return count;
}
//+------------------------------------------------------------------+
//| Check if we should open a new grid (entry signal) |
//+------------------------------------------------------------------+
bool CheckEntrySignal()
{
//-- Trend filter: price above 200 SMA = bullish, below = bearish
double trendSMA = iMA(Symbol(), TrendTF, TrendMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
if(trendSMA == 0) return false; // no data
double currentPrice = MarketInfo(Symbol(), MODE_BID);
bool trendBullish = (currentPrice > trendSMA);
bool trendBearish = (currentPrice < trendSMA);
//-- Entry logic: we look for a pullback to the grid step level
// For simplicity, we use a basic condition: price is within a reasonable range
// This can be enhanced with additional filters like RSI or MACD
//-- We'll enter on the first bar of a new trend when price is not overextended
static datetime lastSignalBar = 0;
if(Time[0] == lastSignalBar) return false;
// Check if price is within 20% of the grid step from a recent swing
double recentHigh = High[iHighest(Symbol(), PERIOD_M15, MODE_HIGH, 20, 1)];
double recentLow = Low[iLowest(Symbol(), PERIOD_M15, MODE_LOW, 20, 1)];
double range = recentHigh - recentLow;
if(range <= 0) return false;
// Only enter if there's sufficient volatility (ATR > minimum threshold)
if(currentATR < 50 * Point()) return false;
// Final entry conditions
if(trendBullish && currentPrice < recentLow + range * 0.2)
{
lastSignalBar = Time[0];
return true;
}
else if(trendBearish && currentPrice > recentHigh - range * 0.2)
{
lastSignalBar = Time[0];
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Open the first order of a new grid |
//+------------------------------------------------------------------+
void OpenFirstOrder()
{
double price = 0;
int cmd = -1;
double sl = 0;
double tp = 0;
double currentBid = MarketInfo(Symbol(), MODE_BID);
double currentAsk = MarketInfo(Symbol(), MODE_ASK);
// Determine direction based on trend
double trendSMA = iMA(Symbol(), TrendTF, TrendMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
if(currentBid > trendSMA)
{
cmd = OP_BUY;
price = currentAsk;
if(StopLossPips > 0) sl = price - StopLossPips * 10 * Point();
// TP is managed by grid target, not per-order
}
else if(currentBid < trendSMA)
{
cmd = OP_SELL;
price = currentBid;
if(StopLossPips > 0) sl = price + StopLossPips * 10 * Point();
}
else
{
return; // no clear trend
}
// Open the order
int ticket = OrderSend(Symbol(), cmd, lotSize, price, Slippage, sl, 0, CustomComment, MagicNumber, 0, clrNONE);
if(ticket > 0)
{
Print("Grid initiated: ", (cmd == OP_BUY ? "BUY" : "SELL"), " at ", price);
gridCount = 1;
isGridActive = true;
}
else
{
Print("Failed to open first order. Error: ", GetLastError());
}
}
//+------------------------------------------------------------------+
//| Check if we need to add a recovery order |
//+------------------------------------------------------------------+
void CheckRecoveryEntry()
{
if(!isGridActive) return;
// Calculate average price of all open grid orders
double avgPrice = GetAveragePrice();
if(avgPrice == 0) return;
double currentPrice = MarketInfo(Symbol(), MODE_BID);
double stepInPoints = gridStepPips * 10 * Point(); // Convert pips to points
int totalOrders = CountGridOrders();
int direction = GetGridDirection();
if(direction == 1) // Buy grid
{
// If price has fallen below avgPrice by more than gridStep * (totalOrders)
double requiredStep = stepInPoints * totalOrders;
if(avgPrice - currentPrice >= requiredStep)
{
// Check S/R protection before opening
if(UseSRProtection)
{
// Don't add if we're too close to a resistance level
double ask = MarketInfo(Symbol(), MODE_ASK);
if(IsNearResistance(ask)) return;
}
OpenRecoveryOrder(OP_BUY);
}
}
else if(direction == -1) // Sell grid
{
// If price has risen above avgPrice by more than gridStep * (totalOrders)
double requiredStep = stepInPoints * totalOrders;
if(currentPrice - avgPrice >= requiredStep)
{
if(UseSRProtection)
{
double bid = MarketInfo(Symbol(), MODE_BID);
if(IsNearSupport(bid)) return;
}
OpenRecoveryOrder(OP_SELL);
}
}
}
//+------------------------------------------------------------------+
//| Get average price of all open grid orders |
//+------------------------------------------------------------------+
double GetAveragePrice()
{
double totalPrice = 0;
double totalLots = 0;
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
totalPrice += OrderOpenPrice() * OrderLots();
totalLots += OrderLots();
count++;
}
}
}
if(totalLots > 0) return totalPrice / totalLots;
return 0;
}
//+------------------------------------------------------------------+
//| Get grid direction: 1 = buy, -1 = sell, 0 = none |
//+------------------------------------------------------------------+
int GetGridDirection()
{
int buyCount = 0, sellCount = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
if(OrderType() == OP_BUY) buyCount++;
else if(OrderType() == OP_SELL) sellCount++;
}
}
}
if(buyCount > 0 && sellCount == 0) return 1;
if(sellCount > 0 && buyCount == 0) return -1;
return 0; // mixed or none
}
//+------------------------------------------------------------------+
//| Open a recovery order |
//+------------------------------------------------------------------+
void OpenRecoveryOrder(int cmd)
{
double price = 0;
double lot = lotSize;
// Calculate lot size based on grid multiplier
int currentCount = CountGridOrders();
if(currentCount > 1)
{
lot = lotSize * MathPow(GridMultiplier, currentCount - 1);
}
lot = NormalizeLot(lot);
double bid = MarketInfo(Symbol(), MODE_BID);
double ask = MarketInfo(Symbol(), MODE_ASK);
if(cmd == OP_BUY)
price = ask;
else if(cmd == OP_SELL)
price = bid;
else
return;
int ticket = OrderSend(Symbol(), cmd, lot, price, Slippage, 0, 0, CustomComment, MagicNumber, 0, clrNONE);
if(ticket > 0)
{
Print("Recovery order opened: ", (cmd == OP_BUY ? "BUY" : "SELL"), " at ", price, " lot: ", lot);
}
else
{
Print("Failed to open recovery order. Error: ", GetLastError());
}
}
//+------------------------------------------------------------------+
//| Check if grid target profit is reached |
//+------------------------------------------------------------------+
bool CheckGridProfitTarget()
{
double totalProfit = 0;
double totalLots = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
totalProfit += OrderProfit() + OrderSwap() + OrderCommission();
totalLots += OrderLots();
}
}
}
// Check if total profit >= target (in pips)
double avgPrice = GetAveragePrice();
if(avgPrice == 0 || totalLots == 0) return false;
double currentPrice = MarketInfo(Symbol(), MODE_BID);
double priceChange = 0;
int direction = GetGridDirection();
if(direction == 1) // Buy grid
priceChange = currentPrice - avgPrice;
else if(direction == -1) // Sell grid
priceChange = avgPrice - currentPrice;
else
return false;
double profitInPips = priceChange / Point() / 10.0;
if(profitInPips >= GridTargetProfit)
{
Print("Grid target reached: ", profitInPips, " pips profit.");
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Close all grid orders |
//+------------------------------------------------------------------+
void CloseAllGridOrders()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
if(OrderType() == OP_BUY)
{
if(!OrderClose(OrderTicket(), OrderLots(), MarketInfo(Symbol(), MODE_BID), Slippage))
Print("Failed to close BUY order: ", GetLastError());
}
else if(OrderType() == OP_SELL)
{
if(!OrderClose(OrderTicket(), OrderLots(), MarketInfo(Symbol(), MODE_ASK), Slippage))
Print("Failed to close SELL order: ", GetLastError());
}
}
}
}
gridCount = 0;
isGridActive = false;
Print("All grid orders closed.");
}
//+------------------------------------------------------------------+
//| Calculate Support and Resistance levels |
//+------------------------------------------------------------------+
void CalculateSRLevels()
{
// Find recent swing highs and lows on the TrendTF
int barsToScan = SR_ScanBars;
if(barsToScan > iBars(Symbol(), TrendTF) - 1) barsToScan = iBars(Symbol(), TrendTF) - 1;
// Find highest high and lowest low
int highestBar = iHighest(Symbol(), TrendTF, MODE_HIGH, barsToScan, 1);
int lowestBar = iLowest(Symbol(), TrendTF, MODE_LOW, barsToScan, 1);
if(highestBar > 0) resistanceLevel = iHigh(Symbol(), TrendTF, highestBar);
if(lowestBar > 0) supportLevel = iLow(Symbol(), TrendTF, lowestBar);
}
//+------------------------------------------------------------------+
//| Check if price is near a resistance level |
//+------------------------------------------------------------------+
bool IsNearResistance(double price)
{
if(resistanceLevel <= 0) return false;
double zone = SR_ZonePips * 10 * Point();
return (MathAbs(price - resistanceLevel) <= zone);
}
//+------------------------------------------------------------------+
//| Check if price is near a support level |
//+------------------------------------------------------------------+
bool IsNearSupport(double price)
{
if(supportLevel <= 0) return false;
double zone = SR_ZonePips * 10 * Point();
return (MathAbs(price - supportLevel) <= zone);
}
//+------------------------------------------------------------------+
//| Floating loss protection |
//+------------------------------------------------------------------+
void CheckFloatingLossProtection()
{
double totalProfit = 0;
double balance = AccountBalance();
if(balance <= 0) return;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
totalProfit += OrderProfit() + OrderSwap() + OrderCommission();
}
}
}
double lossPercent = (-totalProfit / balance) * 100.0;
if(lossPercent >= MaxFloatingLossPct)
{
Print("Floating loss protection triggered: ", lossPercent, "%");
CloseAllGridOrders();
}
}
//+------------------------------------------------------------------+
//| Normalize lot size to broker's step |
//+------------------------------------------------------------------+
double NormalizeLot(double lot)
{
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
if(lotStep == 0) lotStep = 0.01;
double normalized = MathFloor(lot / lotStep) * lotStep;
if(normalized < minLot) normalized = minLot;
// Cap at maximum lot
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
if(normalized > maxLot) normalized = maxLot;
return NormalizeDouble(normalized, 2);
}
//+------------------------------------------------------------------+
```
Reference:
*This article was originally published on FXEAR.com, original content, reproduction without authorization is prohibited.*
Disclaimer: Trading foreign exchange and commodities (including XAUUSD) carries a high level of risk and may not be suitable for all investors. The Expert Advisor provided is for educational and informational purposes only. Past performance does not guarantee future results. Always test any automated trading system on a demo account before deploying it with real funds. The author and FXEAR.com assume no responsibility for any financial losses incurred through the use of this EA. Trade responsibly and only with capital you can afford to lose.