Summary: A trend-following EA for XAUUSD with volatility and session filters. Full MQL4 source code, risk controls, and explanations of gold's unique trading characteristics.




This is not another grid martingale EA. What I'm presenting here is a trend-following system built specifically for the XAUUSD pair, with a volatility filter that I developed after studying BIS reports on gold market liquidity patterns.

Why Gold Needs a Different Approach

Most EAs treat gold like any other currency pair. That's a mistake. Gold has a 24-hour trading cycle, but liquidity is not uniform throughout the day. According to the Bank for International Settlements Triennial Survey, London accounts for roughly 43% of global FX turnover, and precious metals trading is similarly concentrated . During the London-New York overlap (12:00-16:00 UTC), spreads tighten to daily lows, often below $0.30 per ounce.

The volatility structure is also different. A 2025 report from the World Gold Association noted that the 2025 gold price increased by 47% in the first three quarters alone, with a single month (September) gaining 12% . This kind of sustained directional movement punishes mean-reversion strategies and rewards trend-following.

The Strategy Logic

The EA tracks the trend using two EMAs (fast and slow) on the M15 timeframe. Entry signals are generated when the fast EMA crosses above the slow EMA (long) or below (short). But the actual entry only executes after two additional filters pass:

  • <strong>ATR Volatility Filter</strong>: The ATR value must be above a minimum threshold (0.5x of the average ATR over the last 100 bars). This prevents trading in ultra-quiet conditions where spreads become a significant cost.

  • <strong>Session Filter</strong>: The EA only trades during the London session (08:00-16:00 server time, which I adjust based on my broker) and the London-New York overlap. I skip the Asian session entirely because the lower liquidity often produces erratic, directionless movement.


  • The ATR filter is non-negotiable. I got this idea from a paper titled "Volatility Management in Commodity Trading" published by the CFA Institute Research Foundation, which argued that trading strategies with volatility filters outperform their unfiltered counterparts during high-volatility regimes.

    Risk Controls

    Each trade has a fixed stop-loss and take-profit based on ATR multiples. I don't use a trailing stop because gold's sharp reversals tend to hit trailing stops prematurely. Instead, I use a partial take-profit system: 50% of the position closes at the initial target, and the remaining 50% closes when the fast EMA crosses back below the slow EMA (for longs).

    Maximum daily loss: 5% of the starting balance. If reached, the EA stops trading until the next day.

    The Code

    ``cpp
    //+------------------------------------------------------------------+
    //| GoldTrendEA.mq4 |
    //| Developed for |
    //| XAUUSD |
    //+------------------------------------------------------------------+
    #property copyright "FXEAR.com"
    #property link "https://www.fxear.com"
    #property version "1.00"
    #property strict

    //+------------------------------------------------------------------+
    //| Input Parameters - All configurable |
    //+------------------------------------------------------------------+
    // --- Trend Parameters ---
    input int FastMAPeriod = 9; // Fast EMA period (recommended 5-14)
    input int SlowMAPeriod = 21; // Slow EMA period (recommended 20-50)
    input int ATRPeriod = 14; // ATR calculation period
    input double ATRMinMultiplier = 0.5; // Min ATR threshold (0.3-1.0, lower = more entries)

    // --- Trade Parameters ---
    input double RiskPerTrade = 1.0; // Risk per trade as % of equity (0.5-2.0)
    input double StopLossATRMulti = 1.5; // Stop-loss as ATR multiple (1.0-2.5)
    input double TakeProfitATRMulti = 2.5; // Take-profit as ATR multiple (1.5-4.0)
    input double PartialCloseRatio = 0.5; // Ratio to close at first target (0.3-0.7)

    // --- Session Parameters ---
    input int StartHour = 8; // Trading session start hour (server time)
    input int EndHour = 16; // Trading session end hour (server time)
    input bool UseSessionFilter = true; // Enable session filter

    // --- Risk Management ---
    input double MaxDailyLossPct = 5.0; // Max daily loss % (0 = disabled)
    input int MagicNumber = 20250821; // EA magic number

    // --- Order Management ---
    input int Slippage = 30; // Allowed slippage in points
    input string OrderComment = "GoldTrend"; // Order comment

    //+------------------------------------------------------------------+
    //| Global variables |
    //+------------------------------------------------------------------+
    double g_atrValue = 0.0;
    double g_atrAverage = 0.0;
    double g_dailyLossLimit = 0.0;
    double g_dailyStartBalance = 0.0;
    bool g_tradingEnabled = true;

    //+------------------------------------------------------------------+
    //| Expert initialization function |
    //+------------------------------------------------------------------+
    int OnInit()
    {
    // Validate input parameters
    if(FastMAPeriod >= SlowMAPeriod)
    {
    Print("Error: Fast MAPeriod must be less than Slow MAPeriod");
    return(INIT_PARAMETERS_INCORRECT);
    }

    if(RiskPerTrade <= 0 || RiskPerTrade > 5.0)
    {
    Print("Error: RiskPerTrade must be between 0.5 and 5.0");
    return(INIT_PARAMETERS_INCORRECT);
    }

    g_dailyStartBalance = AccountBalance();
    g_dailyLossLimit = g_dailyStartBalance (MaxDailyLossPct / 100.0);

    Print("GoldTrend EA initialized on ", Symbol());
    Print("Daily loss limit: ", g_dailyLossLimit);

    return(INIT_SUCCEEDED);
    }

    //+------------------------------------------------------------------+
    //| Expert deinitialization function |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
    Print("GoldTrend EA deinitialized. Reason: ", reason);
    }

    //+------------------------------------------------------------------+
    //| Expert tick function |
    //+------------------------------------------------------------------+
    void OnTick()
    {
    // Check if trading is allowed
    if(!g_tradingEnabled)
    {
    // Check if we should re-enable trading (new day)
    if(TimeDayOfYear(TimeCurrent()) != TimeDayOfYear(TimeCurrent() - PeriodSeconds(PERIOD_D1)))
    {
    g_tradingEnabled = true;
    g_dailyStartBalance = AccountBalance();
    g_dailyLossLimit = g_dailyStartBalance
    (MaxDailyLossPct / 100.0);
    Print("New day detected. Trading re-enabled. Balance: ", g_dailyStartBalance);
    }
    else
    {
    return; // Still disabled for today
    }
    }

    // Check daily loss limit
    if(MaxDailyLossPct > 0)
    {
    double currentEquity = AccountEquity();
    double loss = g_dailyStartBalance - currentEquity;
    if(loss > g_dailyLossLimit)
    {
    Print("Daily loss limit reached: ", loss, " / ", g_dailyLossLimit);
    g_tradingEnabled = false;
    CloseAllOrders();
    return;
    }
    }

    // Session filter
    if(UseSessionFilter && !IsTradingSession())
    {
    // Print("Outside trading session: ", TimeToString(TimeCurrent()));
    return;
    }

    // Calculate indicators
    double fastEMA = iMA(Symbol(), PERIOD_M15, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 0);
    double slowEMA = iMA(Symbol(), PERIOD_M15, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 0);
    double prevFastEMA = iMA(Symbol(), PERIOD_M15, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);
    double prevSlowEMA = iMA(Symbol(), PERIOD_M15, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);

    // Check if values are valid
    if(fastEMA <= 0 || slowEMA <= 0 || prevFastEMA <= 0 || prevSlowEMA <= 0)
    return;

    // Update ATR values
    g_atrValue = iATR(Symbol(), PERIOD_M15, ATRPeriod, 0);
    g_atrAverage = CalculateATRAverage();

    // Volatility filter
    if(g_atrValue < g_atrAverage ATRMinMultiplier)
    {
    // Print("ATR below threshold: ", g_atrValue, " < ", g_atrAverage
    ATRMinMultiplier);
    return;
    }

    // Check for existing positions
    int positionCount = CountPositions();

    if(positionCount == 0)
    {
    // Look for entry signals
    if(fastEMA > slowEMA && prevFastEMA <= prevSlowEMA)
    {
    // Bullish crossover
    EnterTrade(OP_BUY);
    }
    else if(fastEMA < slowEMA && prevFastEMA >= prevSlowEMA)
    {
    // Bearish crossover
    EnterTrade(OP_SELL);
    }
    }
    else
    {
    // Check if we need to manage existing positions
    ManagePositions(fastEMA, slowEMA);
    }
    }

    //+------------------------------------------------------------------+
    //| Check if current time is within trading session |
    //+------------------------------------------------------------------+
    bool IsTradingSession()
    {
    datetime currentTime = TimeCurrent();
    int hour = TimeHour(currentTime);
    int minute = TimeMinute(currentTime);

    int currentMinutes = hour 60 + minute;
    int startMinutes = StartHour
    60;
    int endMinutes = EndHour 60;

    if(currentMinutes >= startMinutes && currentMinutes <= endMinutes)
    return true;

    return false;
    }

    //+------------------------------------------------------------------+
    //| Calculate average ATR over the last 100 bars |
    //+------------------------------------------------------------------+
    double CalculateATRAverage()
    {
    double sum = 0.0;
    int count = 0;

    for(int i = 1; i <= 100; i++)
    {
    double atr = iATR(Symbol(), PERIOD_M15, ATRPeriod, i);
    if(atr > 0)
    {
    sum += atr;
    count++;
    }
    }

    if(count == 0)
    return g_atrValue; // Fallback

    return sum / count;
    }

    //+------------------------------------------------------------------+
    //| Count open positions with our magic number |
    //+------------------------------------------------------------------+
    int CountPositions()
    {
    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;
    }

    //+------------------------------------------------------------------+
    //| Enter a new trade |
    //+------------------------------------------------------------------+
    void EnterTrade(int tradeType)
    {
    // Calculate position size based on risk
    double stopLossPips = g_atrValue
    StopLossATRMulti / Point;
    if(stopLossPips <= 0)
    return;

    double lotSize = CalculateLotSize(stopLossPips);
    if(lotSize <= 0)
    return;

    // Get current prices
    double bid = Bid;
    double ask = Ask;

    if(tradeType == OP_BUY)
    {
    double sl = ask - stopLossPips Point;
    double tp = ask + g_atrValue
    TakeProfitATRMulti;

    int ticket = OrderSend(Symbol(), OP_BUY, lotSize, ask, Slippage, sl, tp, OrderComment, MagicNumber, 0, clrNONE);
    if(ticket > 0)
    {
    Print("BUY opened. Ticket: ", ticket, " Lot: ", lotSize, " SL: ", sl, " TP: ", tp);
    }
    else
    {
    Print("BUY failed. Error: ", GetLastError());
    }
    }
    else if(tradeType == OP_SELL)
    {
    double sl = bid + stopLossPips Point;
    double tp = bid - g_atrValue
    TakeProfitATRMulti;

    int ticket = OrderSend(Symbol(), OP_SELL, lotSize, bid, Slippage, sl, tp, OrderComment, MagicNumber, 0, clrNONE);
    if(ticket > 0)
    {
    Print("SELL opened. Ticket: ", ticket, " Lot: ", lotSize, " SL: ", sl, " TP: ", tp);
    }
    else
    {
    Print("SELL failed. Error: ", GetLastError());
    }
    }
    }

    //+------------------------------------------------------------------+
    //| Calculate lot size based on risk percentage |
    //+------------------------------------------------------------------+
    double CalculateLotSize(double stopLossPips)
    {
    if(stopLossPips <= 0)
    return 0.0;

    double riskAmount = AccountBalance() (RiskPerTrade / 100.0);
    double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
    double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);

    // Calculate lot size: riskAmount / (stopLossPips
    tickValue)
    double lotSize = riskAmount / (stopLossPips tickValue);

    // Round to the nearest lot step
    if(lotStep > 0)
    {
    lotSize = MathFloor(lotSize / lotStep)
    lotStep;
    }

    // Check min and max lot
    double minLot = MarketInfo(Symbol(), MODE_MINLOT);
    double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);

    if(lotSize < minLot)
    lotSize = minLot;
    if(lotSize > maxLot)
    lotSize = maxLot;

    return NormalizeDouble(lotSize, 2);
    }

    //+------------------------------------------------------------------+
    //| Manage existing positions |
    //+------------------------------------------------------------------+
    void ManagePositions(double fastEMA, double slowEMA)
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
    continue;

    // Check if we should close based on EMA crossover
    bool closeSignal = false;

    if(OrderType() == OP_BUY)
    {
    if(fastEMA < slowEMA) // Bearish crossover - exit long
    closeSignal = true;
    }
    else if(OrderType() == OP_SELL)
    {
    if(fastEMA > slowEMA) // Bullish crossover - exit short
    closeSignal = true;
    }

    if(closeSignal)
    {
    CloseOrder(OrderTicket());
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| Close a specific order |
    //+------------------------------------------------------------------+
    bool CloseOrder(int ticket)
    {
    if(OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES))
    {
    int type = OrderType();
    double price;
    int slippage = Slippage;

    if(type == OP_BUY)
    price = Bid;
    else if(type == OP_SELL)
    price = Ask;
    else
    return false;

    bool result = OrderClose(ticket, OrderLots(), price, slippage, clrNONE);
    if(result)
    {
    Print("Order closed. Ticket: ", ticket, " Price: ", price);
    return true;
    }
    else
    {
    Print("Order close failed. Ticket: ", ticket, " Error: ", GetLastError());
    return false;
    }
    }
    return false;
    }

    //+------------------------------------------------------------------+
    //| Close all open orders |
    //+------------------------------------------------------------------+
    void CloseAllOrders()
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
    {
    CloseOrder(OrderTicket());
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| Check for partial take-profit |
    //+------------------------------------------------------------------+
    void CheckPartialTakeProfit()
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
    continue;

    double profitPips = 0;
    if(OrderType() == OP_BUY)
    profitPips = (Bid - OrderOpenPrice()) / Point;
    else if(OrderType() == OP_SELL)
    profitPips = (OrderOpenPrice() - Ask) / Point;

    double targetPips = g_atrValue TakeProfitATRMulti / Point;
    if(targetPips > 0 && profitPips >= targetPips
    PartialCloseRatio)
    {
    // Close half position
    double closeLots = NormalizeDouble(OrderLots() * PartialCloseRatio, 2);
    if(closeLots >= MarketInfo(Symbol(), MODE_MINLOT))
    {
    double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
    if(OrderClose(OrderTicket(), closeLots, closePrice, Slippage, clrNONE))
    {
    Print("Partial close executed. Remaining lots: ", OrderLots() - closeLots);
    }
    }
    }
    }
    }
    }
    //+------------------------------------------------------------------+
    ``

    Loading Instructions

    Load this EA on the XAUUSD chart with M15 timeframe. Why M15? It provides enough data points for the EMA crossover to produce meaningful signals without the noise of lower timeframes, while still being responsive enough to capture significant gold moves.

    A Note on Session Times

    The default session times (8-16 server time) target the London session. Check your broker's server time offset. If your broker uses GMT+2, 8-16 server time corresponds to 6-14 UTC. If you're in a different timezone, adjust accordingly.

    References

  • Bank for International Settlements. (2022). <em>Triennial Central Bank Survey of Foreign Exchange and Over-the-counter Derivatives Markets</em>.

  • CFA Institute Research Foundation. (2020). <em>Volatility Management in Commodity Trading</em>.

  • World Gold Council. (2025). <em>Gold Market Commentary - September 2025</em>.


  • 本文首发于FXEAR.com(https://www.fxear.com),原创内容,未经授权禁止转载。

    Disclaimer: Trading gold (XAUUSD) and other financial instruments involves significant risk. Past performance does not guarantee future results. This EA is provided for educational purposes only. Always test thoroughly on a demo account before using on a live account. The author and FXEAR.com accept no responsibility for any financial losses incurred through the use of this software. Only trade with capital you can afford to lose.Text to display