How to Build an ATR-Based Position Sizing EA in MQL4

Learn to build a robust ATR-based position sizing EA in MQL4 that dynamically adjusts lot sizes to market volatility, protecting your account from drawdowns.

how-to-build-an-atr-based-position-sizing-ea-in

Why Most Traders Get Position Sizing Wrong

I've been building EAs for over a decade, and the single most common mistake I see in amateur algorithms is fixed lot sizing. A trader writes OrderSend(Symbol(), OP_BUY, 0.1, ...) and calls it a day. This is financial suicide when volatility spikes or account equity shrinks. The market doesn't care about your fixed 0.1 lot—it cares about the risk you're taking relative to the current environment.

Professional traders use ATR (Average True Range) for position sizing because it adapts dynamically to market conditions. When volatility expands, your position size shrinks—keeping your dollar risk constant. When volatility contracts, you scale up. This is the foundation of robust risk management, and it's surprisingly simple to implement in MQL4.

The fixed-lot approach is particularly dangerous during high-impact news events like NFP or FOMC announcements. I've watched traders blow accounts because they kept 0.5 lots on EUR/USD when volatility quadrupled overnight. The market doesn't warn you—it just moves, and your stop loss becomes a guaranteed loss instead of a calculated risk.

Understanding ATR for Position Sizing

ATR measures market volatility by calculating the average range between high and low prices over a specified period. For position sizing, we use ATR to determine how many pips we're willing to risk per trade, then calculate the lot size that keeps that risk within a fixed percentage of our account equity.

The core formula is straightforward:

// Risk amount in account currency
double riskAmount = AccountBalance() * riskPercent / 100;

// Stop loss in pips (converted from ATR value)
double stopLossPips = atrValue * stopLossMultiplier;

// Pip value depends on lot size and instrument
double pipValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double lotSize = riskAmount / (stopLossPips * pipValue);

This ensures that whether EUR/USD is moving 10 pips per day or 50, your maximum loss per trade stays at exactly 1% of your account (or whatever percentage you choose).

One nuance many beginners miss: ATR is denominated in points, not pips. On a 5-digit broker, 1 pip = 10 points. Your ATR value from iATR() is in points, so you must handle this conversion correctly. The code above uses Point which is the minimum price movement—typically 0.0001 for 4-digit pairs or 0.00001 for 5-digit pairs. Always verify your broker's point value before using this in production.

Why ATR Over Other Volatility Measures

You might wonder why I recommend ATR instead of Bollinger Bands, standard deviation, or average true range percentage. Here's my reasoning:

  • Simplicity: ATR is a single value, easy to understand and compute. No need for multiple parameters like Bollinger Bands' standard deviation multiplier.
  • Direct applicability: ATR directly measures price movement in the same units as your stop loss. Standard deviation requires conversion to price units.
  • Historical reliability: ATR has been used by professional traders since Wilder introduced it in 1978. It's battle-tested across decades of market regimes.
  • MT4 native support: The iATR() function is built into MQL4, requiring no custom indicators or external libraries.

The only real competitor is ATR% (ATR divided by price), which accounts for percentage moves. I prefer raw ATR for stop loss placement because stops are in price units, not percentages. But if you trade instruments with widely different prices (e.g., EUR/USD vs. Bitcoin), you might want to normalize with ATR% for cross-comparison.

Building the ATR Position Sizing EA in MQL4

Let's build a complete EA that uses ATR for dynamic position sizing. I'll walk through each component, explaining why specific choices matter for real trading.

EA Input Parameters

The first step is defining our input parameters. These should be configurable without recompiling the EA:

Parameter Type Default Description
RiskPercent double 1.0 Percentage of account balance to risk per trade
ATRPeriod int 14 Number of bars for ATR calculation
StopLossMultiplier double 2.0 Multiplier applied to ATR to set stop loss distance
MaxLotSize double 10.0 Maximum lot size allowed (safety cap)
MagicNumber int 123456 Unique identifier for this EA's orders

Notice I've included MaxLotSize as a safety cap. Even if your risk calculation says 50 lots, the broker won't accept it, and you don't want your EA throwing errors. Always clamp to realistic values. I also recommend adding an AccountProtection input that prevents trading if drawdown exceeds a certain percentage—but that's an advanced topic for another post.

The Core Position Sizing Function

Here's the heart of our EA—the function that calculates lot size based on ATR. This is production-ready code I've used in live trading:

//+------------------------------------------------------------------+
//| Calculate lot size based on ATR and risk percentage              |
//+------------------------------------------------------------------+
double CalculateLotSize()
{
   // Get current ATR value
   double atrValue = iATR(Symbol(), PERIOD_CURRENT, ATRPeriod, 1);
   if(atrValue <= 0) return(0.01);
   
   // Calculate stop loss distance in points
   double stopLossPoints = atrValue * StopLossMultiplier;
   
   // Convert to absolute price distance
   double stopLossDistance = stopLossPoints * Point;
   
   // Calculate risk amount in account currency
   double riskAmount = AccountBalance() * RiskPercent / 100.0;
   
   // Get tick value (pip value for 1 lot)
   double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
   if(tickValue <= 0) return(0.01);
   
   // Calculate raw lot size
   double rawLotSize = riskAmount / (stopLossDistance / Point * tickValue);
   
   // Apply broker lot step constraints
   double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
   double minLot = MarketInfo(Symbol(), MODE_MINLOT);
   double maxLot = MathMin(MarketInfo(Symbol(), MODE_MAXLOT), MaxLotSize);
   
   // Round down to nearest valid lot step
   double lotSize = MathFloor(rawLotSize / lotStep) * lotStep;
   
   // Clamp to broker limits
   lotSize = MathMax(minLot, MathMin(maxLot, lotSize));
   
   return(lotSize);
}

Notice the safety checks: we validate that ATR and tick values are positive before calculating, and we respect broker lot limits. The MathFloor rounding ensures we never risk more than intended—always round down, never up.

A common edge case: when stopLossPoints becomes very small (e.g., during extremely low volatility on a 5-digit broker), the calculated lot size can be astronomically high. The MaxLotSize cap prevents this, but you should also consider adding a minimum stop loss distance check. I've seen EAs try to open 100 lots because ATR dropped to 5 points on a quiet Sunday. Always sanity-check your inputs.

Entry and Exit Logic

For demonstration, let's add a simple trend-following entry based on a moving average crossover. The key is how we use ATR for both position sizing and stop loss placement:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // Only check for new signals on new bar
   static datetime lastBarTime = 0;
   if(Time[0] == lastBarTime) return;
   lastBarTime = Time[0];
   
   // Check for existing position
   if(CountOpenOrders() > 0) return;
   
   // Get moving averages
   double fastMA = iMA(Symbol(), PERIOD_CURRENT, 10, 0, MODE_EMA, PRICE_CLOSE, 1);
   double slowMA = iMA(Symbol(), PERIOD_CURRENT, 30, 0, MODE_EMA, PRICE_CLOSE, 1);
   double prevFastMA = iMA(Symbol(), PERIOD_CURRENT, 10, 0, MODE_EMA, PRICE_CLOSE, 2);
   double prevSlowMA = iMA(Symbol(), PERIOD_CURRENT, 30, 0, MODE_EMA, PRICE_CLOSE, 2);
   
   // Calculate ATR-based stop loss distance
   double atrValue = iATR(Symbol(), PERIOD_CURRENT, ATRPeriod, 1);
   double stopLossPips = atrValue * StopLossMultiplier;
   double stopLossPoints = stopLossPips * Point;
   
   // Buy signal: fast MA crosses above slow MA
   if(prevFastMA <= prevSlowMA && fastMA > slowMA)
   {
      double lotSize = CalculateLotSize();
      if(lotSize <= 0) return;
      
      double sl = Bid - stopLossPoints;
      double tp = Bid + (stopLossPoints * 2.0); // 1:2 risk-reward
      
      int ticket = OrderSend(Symbol(), OP_BUY, lotSize, Ask, 3, sl, tp, 
                             "ATR Sizing EA", MagicNumber, 0, clrGreen);
      if(ticket > 0)
         Print("Buy order placed: Lot=", lotSize, " SL=", sl, " TP=", tp);
   }
   
   // Sell signal: fast MA crosses below slow MA
   if(prevFastMA >= prevSlowMA && fastMA < slowMA)
   {
      double lotSize = CalculateLotSize();
      if(lotSize <= 0) return;
      
      double sl = Ask + stopLossPoints;
      double tp = Ask - (stopLossPoints * 2.0);
      
      int ticket = OrderSend(Symbol(), OP_SELL, lotSize, Bid, 3, sl, tp,
                             "ATR Sizing EA", MagicNumber, 0, clrRed);
      if(ticket > 0)
         Print("Sell order placed: Lot=", lotSize, " SL=", sl, " TP=", tp);
   }
}

//+------------------------------------------------------------------+
//| Count open orders with this EA's magic number                    |
//+------------------------------------------------------------------+
int CountOpenOrders()
{
   int count = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderMagicNumber() == MagicNumber && OrderSymbol() == Symbol())
            count++;
      }
   }
   return(count);
}

One important detail: I use Time[0] == lastBarTime to prevent multiple signals on the same bar. Without this check, your EA might open dozens of trades during a single tick when the crossover condition is true. This is a classic MQL4 bug that destroys accounts. Always implement new-bar detection for any EA that uses indicator-based signals.

Also note that the stop loss is placed at Bid - stopLossPoints for buys and Ask + stopLossPoints for sells. This accounts for the spread. If your broker has variable spreads, the actual stop distance might be slightly different than calculated. For ultra-precision, you can add the current spread to your stop loss calculation, but I find it's unnecessary for most strategies.

Handling Multiple Positions and Hedging

If your strategy uses multiple

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles