Grid Trading EA MQL5: Code & Risk Controls

Build a grid trading EA MQL5 with dynamic spacing and hard risk limits. See working code, input parameters, and honest backtest pitfalls.

grid-trading-ea-mql5-code-risk-controls

Let's be blunt: most grid trading EAs you'll find on forums are either martingale in disguise or they blow up accounts with quiet, mechanical certainty. That doesn't mean grids are worthless—it means most people code them without thinking about what happens when the market stops trending and starts ranging, or worse, trends violently against them. I've spent years writing and testing MQL5 EAs, and the grid is one of the few strategies where the difference between a well-built system and a reckless one is measured in weeks, not years.

This post is for traders who want to build a grid trading EA MQL5 that doesn't rely on hope. You'll get working code for dynamic grid spacing, a complete risk control framework, and the honest truth about where grids fail. You'll also learn exactly how to optimize it in the Strategy Tester without fooling yourself into thinking a 400% backtest means anything.

What a Grid Trading Strategy Actually Does

A grid trading strategy places buy and sell orders at predefined price levels above and below the current price. When price hits a level, the EA opens a position. If price keeps moving in one direction, you're making money on every level it passes. If price reverses, you're holding losing positions until it comes back—or until your margin runs out.

That's the entire concept. There's no magic indicator, no complex entry logic. The grid's profitability depends entirely on two things: how you space the levels and how you manage the risk when price goes against you.

Most grid EAs use fixed spacing—say, 20 pips between each level. That's simple, but it's also rigid. In a low-volatility session, 20 pips might take hours to reach. In a news spike, price can blow through five levels in a minute, leaving you with a pile of open positions and a floating loss that looks like a phone number.

Dynamic spacing solves this by adjusting the distance between levels based on current volatility. When the market is calm, levels are closer together so the grid actually trades. When volatility spikes, levels widen so you don't get stacked into a position that's instantly deep underwater.

Why Dynamic Grid Spacing Matters

Here's the thing about fixed grids: they have a hidden assumption that the market's average range is constant. It isn't. EURUSD might move 15 pips in an hour during the Asian session and 80 pips in the same hour during London/NY overlap. A fixed grid of 20 pips is either too wide for the quiet hours (no trades, capital idle) or too narrow for the active hours (instant overload).

Dynamic spacing uses a volatility measure—usually the Average True Range (ATR)—to set the distance between levels. The formula is simple:

double atr = iATR(_Symbol, PERIOD_CURRENT, atrPeriod, 0);
double spacing = atr * atrMultiplier;

With atrMultiplier set to 1.0, your grid spacing equals the current ATR. If ATR is 25 pips, levels are 25 pips apart. If ATR jumps to 60 pips during a news event, the next level is now 60 pips away. You take fewer trades in volatile conditions, but each trade has more room to breathe.

I prefer using the current bar's ATR rather than a smoothed value, because it reacts faster to volatility changes. The tradeoff is that it can be noisy. If you find your grid opening and closing levels erratically, switch to the ATR of the previous closed bar (iATR(_Symbol, PERIOD_CURRENT, atrPeriod, 1)).

Building the MQL5 Grid Bot: Core Code

Let's write the foundation of a MQL5 grid bot. I'm going to keep the code focused on the grid logic and risk controls, not the full EA with every input imaginable. You'll need to add your own position management and cleanup, but this gives you the skeleton.

First, the inputs. These are the parameters you'll see in the EA's input dialog. Every one of them matters, and I'll explain the risky ones in detail.

ParameterTypeDefaultDescription
GridDirectionenumBOTHBOTH, BUY_ONLY, or SELL_ONLY. One-directional grids are safer for trending markets.
ATRPeriodint14ATR period for dynamic spacing. Lower values react faster but are noisier.
ATRMultiplierdouble1.5Multiplier applied to ATR. 1.5 means spacing = 1.5 x ATR. Higher = wider grid.
MaxGridLevelsint10Maximum number of open positions per direction. Hard stop on grid depth.
LotSizedouble0.01Base lot size for each grid level. Fixed lot keeps risk linear.
MaxDrawdownPercentdouble20.0EA closes all positions if equity drops this % from peak. Non-negotiable.
TakeProfitPipsint30Take profit in pips for each individual position. Close each level independently.

Now the core logic. On every new bar, the EA checks if price has moved far enough from the last placed order to justify a new one. Here's the function that checks and places a buy grid order:

void CheckAndPlaceBuyOrder()
{
   // Get current ATR for dynamic spacing
   double atr = iATR(_Symbol, PERIOD_CURRENT, ATRPeriod, 0);
   double spacing = atr * ATRMultiplier * _Point * 10; // convert to price

   // Find the lowest open buy price
   double lowestBuy = DBL_MAX;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
      {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC) == MagicNumber &&
            PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            double price = PositionGetDouble(POSITION_PRICE_OPEN);
            if(price < lowestBuy) lowestBuy = price;
         }
      }
   }

   // Count open buy positions
   int buyCount = CountOpenBuyPositions();

   // If no buys exist, place one at market. Otherwise, place below the lowest.
   double placePrice;
   if(buyCount == 0)
      placePrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   else
      placePrice = lowestBuy - spacing;

   // Check max levels
   if(buyCount >= MaxGridLevels) return;

   // Place the order
   MqlTradeRequest request = {};
   MqlTradeResult result = {};
   request.action = TRADE_ACTION_DEAL;
   request.symbol = _Symbol;
   request.volume = LotSize;
   request.type = ORDER_TYPE_BUY_LIMIT;
   request.price = NormalizeDouble(placePrice, _Digits);
   request.deviation = 10;
   request.magic = MagicNumber;
   request.comment = "GridBuy";
   OrderSend(request, result);
}

Notice I'm using ORDER_TYPE_BUY_LIMIT rather than market orders. This is deliberate. Limit orders let the grid "wait" at levels instead of chasing price. If price gaps through a level, the limit order still fills at the level or better—you don't get slippage. Market orders in a fast-moving market will fill you at whatever price is available, which can turn a 20-pip grid into a 35-pip one instantly.

The sell side mirrors this logic with ORDER_TYPE_SELL_LIMIT above the highest open sell.

Risk Controls That Actually Save You

Now here's where most grids die. The code above will happily open 10, 20, 50 positions if price trends. Without controls, a 100-pip trend on EURUSD with a 20-pip grid means five open positions, each losing more than the last. That's not a bug; that's the grid's nature.

The first control is MaxGridLevels. This is a hard cap on the number of positions per direction. When you hit it, the EA stops opening new levels. You're not adding to a losing position indefinitely. I set this based on the maximum historical drawdown of the currency pair. For EURUSD, 10-15 levels is usually enough. For GBPJPY, I wouldn't go above 8—that pair can trend 300 pips without blinking.

The second control is MaxDrawdownPercent. This is your circuit breaker. The EA tracks the highest equity it's seen and closes everything if current equity drops below that peak by the specified percentage. This is the difference between a grid that loses 20% and one that loses your entire account. When the breakers trips, the EA closes all positions and stops trading. You have to manually restart it. That's intentional—if the market just ran 200 pips against you, you don't want the EA reopening positions in the same conditions.

The third control is less obvious: Take profit per position, not per grid. Many grid EAs use a single take profit for the whole basket—when total profit hits X, close everything. That's fine, but it means you're holding losing positions for a long time waiting for the basket to reach breakeven. I prefer closing each position at its own take profit. This locks in gains from the levels that are in profit and reduces the size of the basket over time.

Pros, Cons, and Risks: An Honest Assessment

Grid trading isn't a scam, and it isn't a money printer. It's a strategy with specific conditions where it works and specific conditions where it fails catastrophically.

Aspect

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