Risk Management in EA — Protect Your Capital with MetaTrader 5
Your EA can find perfect entries, but without risk management one bad trade can wipe out weeks of gains. According to the MQL5 Trade Functions documentation, every order you send must include sensible volume, Stop Loss, and Take Profit — or you risk blowing your account. This guide teaches you position sizing, SL/TP, trailing stops, and account safeguards using the official MetaTrader 5 API, so your EA trades safely in demo and live.
Why Risk Management Matters
A strategy that wins 60% of the time can still lose money if losses are bigger than wins. Risk management controls how much you risk per trade and where you cut losses. Per MqlTradeRequest: volume, sl, and tp are core fields — use them wisely.
What we'll add to your EA:
| Topic | Why It Matters |
|---|---|
| Position sizing | Decide lot size per trade — fixed, % risk, or ATR-based |
| Stop Loss (SL) | Cap loss per trade; broker executes when price hits SL |
| Take Profit (TP) | Lock in profit; exit when price reaches TP |
| Trailing stop | Move SL in profit direction to protect gains |
| Account safeguards | Max positions, margin check, daily loss limit |
Step 1: Fixed Lot Sizing
What is this? Lot size is how many units (lots) you trade. A fixed lot means you always open 0.1, 0.5, or 1.0 lots — regardless of account size or market volatility. It's the easiest way to get started.
Why use it? For backtesting and simple strategies, fixed lot keeps the logic straightforward. You don't need to calculate anything — just set an input and use it.
Important: Every broker defines minimum and maximum lot sizes (e.g. min 0.01, max 100). If you send 0.001 or 500 lots, the broker rejects the order. You must read these limits first.
MqlTradeRequest.volume expects the volume in lots. Per SymbolInfoDouble:
double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
// Clamp lot to broker limits
double lot = MathMax(volMin, MathMin(volMax, InpLotSize));
lot = MathFloor(lot / volStep) * volStep; // Round to step
lot = NormalizeDouble(lot, 2); // 2 decimals typical for lots
Apply before OrderSend: request.volume = lot;
Step 2: Risk Per Trade (%)
What is this? Instead of a fixed lot, you risk a fixed percentage of your account per trade. For example: "I will never risk more than 1% of my balance on a single trade." If your balance is $10,000, each trade risks at most $100. If your balance grows to $20,000, each trade now risks $200 — your lot size scales automatically.
Why use it? A 0.1 lot on a $1,000 account is huge (can wipe you out in a few bad trades). The same 0.1 lot on $50,000 is small. Risk % keeps your risk proportional to your account — so you survive drawdowns and grow steadily.
How it works: You decide: "I risk 1% per trade." Then you calculate: "If my Stop Loss is 50 pips away, how many lots can I open so that 50 pips = 1% of my balance?" The answer gives you the lot size. Use AccountInfoDouble(ACCOUNT_BALANCE) and tick value (or OrderCalcProfit) for the calculation.
Formula:
- Risk amount = Balance × (RiskPercent / 100)
- Lot = Risk amount / (StopLoss points × Point value per lot)
Example — 1% risk, 50-pip SL on EURUSD:
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskPct = 1.0; // 1%
double riskAmount = balance * (riskPct / 100.0);
double slPoints = 50 * 10; // 50 pips = 500 points (5-digit quote)
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
// Value per point per 1 lot (simplified; use OrderCalcProfit for precision)
double valuePerPoint = (tickValue / tickSize) * point;
double lot = riskAmount / (slPoints * point * valuePerPoint);
// Clamp to symbol limits
lot = MathMax(volMin, MathMin(volMax, lot));
lot = MathFloor(lot / volStep) * volStep;
lot = NormalizeDouble(lot, 2);
Tip: For exact margin/profit checks, use OrderCalcMargin and OrderCalcProfit as in the official docs.
Step 3: ATR-Based Lot Sizing
What is this? ATR (Average True Range) is an indicator that measures how much the price moves on average. On calm days, ATR is small (e.g. 20 pips). On volatile days, ATR is large (e.g. 80 pips). If you use a fixed 50-pip Stop Loss, on volatile days the price can hit your SL easily (false stop-out). On calm days, 50 pips might be too tight and get you stopped out by normal noise.
Why use it? By basing your Stop Loss distance on ATR (e.g. 1.5× ATR), you adapt to current volatility. When the market is calm, your SL is closer. When it's volatile, your SL is wider — so you don't get stopped out by normal swings. Then you use that SL distance in your % risk formula: wider SL → smaller lot for the same dollar risk.
How it works: Get ATR value with iATR and CopyBuffer. Multiply by a factor (e.g. 1.5) to get your SL distance in price. Convert to points and use in your lot calculation.
int atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
double atrBuffer[];
ArraySetAsSeries(atrBuffer, true);
CopyBuffer(atrHandle, 0, 0, 1, atrBuffer);
double atr = atrBuffer[0];
double slDistance = atr * 1.5; // SL = 1.5× ATR
double slPoints = slDistance / SymbolInfoDouble(_Symbol, SYMBOL_POINT);
// Then use slPoints in your % risk formula above
Step 4: Stop Loss and Take Profit
What is this? Stop Loss (SL) and Take Profit (TP) are price levels you set when opening a trade. Stop Loss = "If the price goes against me this far, close the position automatically." Take Profit = "If the price moves in my favor this far, close and lock in profit." The broker executes them for you — you don't need to watch the chart.
Why use them? Without SL, one bad trade can wipe out your account. Without TP, you might never close winners. SL and TP remove emotion — the EA (and broker) handle exits automatically.
Important: In MqlTradeRequest, sl and tp must be prices (e.g. 1.0850), not points. You calculate the price from entry ± (pips × point). Set them when opening:
| Position | SL | TP |
|---|---|---|
| Buy | Below entry price | Above entry price |
| Sell | Above entry price | Below entry price |
Example — Buy with 50-pip SL and 100-pip TP:
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
double sl = NormalizeDouble(ask - 50 * 10 * point, digits); // 50 pips
double tp = NormalizeDouble(ask + 100 * 10 * point, digits); // 100 pips
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = lot;
request.type = ORDER_TYPE_BUY;
request.price = ask;
request.sl = sl;
request.tp = tp;
For Sell: sl = NormalizeDouble(bid + 50*10*point, digits); tp = NormalizeDouble(bid - 100*10*point, digits);
Step 5: SYMBOL_TRADE_STOPS_LEVEL and NormalizeDouble
What is this? Brokers don't allow SL/TP to be placed too close to the current price. For example: you can't put a Stop Loss 1 pip away — brokers require a minimum distance (often 10–50 points, depending on symbol). If you ignore this, OrderSend returns error 130 (invalid stops).
SYMBOL_TRADE_STOPS_LEVEL tells you the minimum distance in points. NormalizeDouble rounds prices to the correct number of decimals — brokers reject prices like 1.0850123 when the symbol expects 1.08501.
Why it matters: Many beginners get "invalid stops" because they either put SL too close or send prices with wrong precision. Always check SYMBOL_TRADE_STOPS_LEVEL and use NormalizeDouble(price, digits) before sending. Per MQL5 Symbol Properties:
int stopLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
if(stopLevel <= 0) stopLevel = 10; // Fallback
double minDist = stopLevel * point;
// For Buy: ensure SL is at least minDist below price
double slDist = MathMax(minDist + 5 * point, 50 * 10 * point); // buffer + your SL
sl = NormalizeDouble(ask - slDist, digits);
tp = NormalizeDouble(ask + tpDist, digits);
Always use NormalizeDouble(price, digits) — brokers reject prices with wrong precision.
Step 6: Trailing Stop
What is this? A trailing stop is a Stop Loss that moves as the price moves in your favor. Example: You buy at 1.0850 with SL at 1.0800. Price rises to 1.0900. Instead of leaving SL at 1.0800, you move it up to 1.0870 (30 pips below current price). Now, if price reverses, you lock in 20 pips profit instead of a loss. As price keeps rising, you keep moving SL up — "trailing" behind the price.
Why use it? It protects profits. Without trailing stop, a winning trade can turn into a loser if you wait too long. With trailing stop, you secure gains as the market moves in your favor.
How it works: In OnTick(), loop through your positions. For a Buy: if the current Bid is far enough above your open price and above your current SL, modify the SL to a new level (e.g. Bid minus 30 pips). The broker then holds the new SL — if price drops, it closes at that level.
double trailPoints = 30 * 10; // 30 pips
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
double newSL = NormalizeDouble(bid - trailPoints * point, digits);
if(newSL > sl && newSL < bid)
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_SLTP;
req.position = ticket;
req.symbol = _Symbol;
req.sl = newSL;
req.tp = PositionGetDouble(POSITION_TP);
OrderSend(req, res);
}
}
// Similar for SELL
}
Step 7: Account Safeguards
What is this? Account safeguards are extra checks you add so your EA doesn't do something dangerous. For example: "Never open more than 2 positions at once" or "Don't open a new trade if free margin is below $500." These rules protect you from bugs (e.g. EA opening 100 positions) or extreme market conditions.
Why use them? Even with good SL/TP, a bug or unexpected event can cause over-trading or over-leveraging. Safeguards are a safety net — they stop the EA before it hurts your account.
Common safeguards:
Max positions: Don't open more than N positions per symbol or total.
int countMyPositions()
{
int n = 0;
for(int i = 0; i < PositionsTotal(); i++)
if(PositionGetInteger(POSITION_MAGIC) == InpMagic) n++;
return n;
}
if(countMyPositions() >= InpMaxPositions) return; // Don't open new
Margin check: Use AccountInfoDouble(ACCOUNT_MARGIN_FREE) and OrderCalcMargin to ensure you have enough margin before opening.
Summary — What You Built
You added: fixed and % risk lot sizing, ATR-based SL distance, SL/TP in MqlTradeRequest, SYMBOL_TRADE_STOPS_LEVEL handling, trailing stop, and account safeguards. Your EA now respects broker limits and protects capital.
Bonus Tip: Want to add these risk rules without writing code? Try AlfaTactix Strategy Builder free — configure position sizing (fixed, % risk, ATR-based), Stop Loss, Take Profit, trailing stops, and account safeguards in a visual interface. Export production-ready MQL5 that respects broker limits. Same protection, zero coding.
Full Code
// Risk-aware order open helper
bool OpenBuyWithSLTP(double lot, double slPips, double tpPips)
{
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
int stopLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
if(stopLevel <= 0) stopLevel = 10;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = NormalizeDouble(ask - MathMax(stopLevel * point, slPips * 10 * point), digits);
double tp = NormalizeDouble(ask + MathMax(stopLevel * point, tpPips * 10 * point), digits);
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = lot;
req.type = ORDER_TYPE_BUY;
req.price = ask;
req.sl = sl;
req.tp = tp;
req.deviation = 10;
req.magic = InpMagic;
return OrderSend(req, res);
}
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Invalid stops (130) | SL/TP too close or wrong side | Use SYMBOL_TRADE_STOPS_LEVEL, NormalizeDouble, and correct Buy/Sell direction. |
Not enough money (134) | Lot too large for margin | Reduce lot, use % risk, or check OrderCalcMargin before send. |
| Lot rounded to zero | Risk % too small or SL too wide | Ensure lot >= SYMBOL_VOLUME_MIN; increase risk % or narrow SL. |
| Trailing stop not moving | Condition never met or modify failed | Log newSL vs sl; check retcode from OrderSend; ensure min distance. |
See MQL5 Trade Return Codes for all error codes.