Let us tell you how accounts actually die.
It's almost never the strategy. We've watched solid, backtested-to-death strategies blow up accounts because someone forgot a drawdown check. Or ran without a spread filter and got eaten alive by widening spreads at midnight. Or stacked ten positions in the same direction because nothing said stop.
After building over a hundred trading algorithms, we can tell you this with certainty: risk management is not a section of your code. It is the code. Everything else is just deciding when to click buy.
Here are ten techniques. All battle-tested. All from production.
Technique 1: ATR-Based Stop Losses
Fixed stops are lazy. Thirty pips might be generous on a sleepy Tuesday. During NFP? That's pocket change – you'll get stopped out by the noise alone.
ATR solves this. Volatile market? Wider stop. Calm market? Tighter stop. The market tells you what's appropriate.
MQL4 Implementation
// Calculate ATR-based stop loss
double atr = iATR(NULL, 0, 14, 1); // 14-period ATR on the last completed bar
double sl_distance = atr * 1.5; // SL = 1.5 × ATR
// For a BUY order:
double stop_loss = Ask - sl_distance;
double take_profit = Ask + (sl_distance * 2.0); // TP = 2:1 reward-to-risk
// Ensure minimum stop distance
double min_sl = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point;
if (sl_distance < min_sl) sl_distance = min_sl;Python Equivalent
import pandas as pd
def atr_stop_loss(df, period=14, multiplier=1.5):
"""Calculate ATR-based stop loss distance."""
high_low = df['high'] - df['low']
high_close = abs(df['high'] - df['close'].shift(1))
low_close = abs(df['low'] - df['close'].shift(1))
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
atr = true_range.rolling(window=period).mean()
stop_distance = atr * multiplier
return stop_distance
# Usage
df['sl_distance'] = atr_stop_loss(df)
df['buy_sl'] = df['close'] - df['sl_distance']
df['buy_tp'] = df['close'] + df['sl_distance'] * 2.0 # 2:1 R:RThe math: $SL_{\text{distance}} = ATR(14) \times 1.5$
A 1.5× multiplier is the sweet spot for most strategies we've built. Go tighter (1.0×) and random wicks eat you alive. Go wider (3.0+) and your risk-per-trade math starts looking ugly.
Technique 2: Risk-Based Position Sizing
Here's a mistake we see constantly: fixed lot sizes. Someone trades 1.0 lots with a 20-pip stop (risking $200) and also 1.0 lots with a 50-pip stop (risking $500). Same lot size, completely different risk. That's not managing risk – that's pretending to.
The fix is brutally simple. Calculate your lot size so that every single trade risks the same dollar amount, regardless of how far away your stop is.
MQL4 Implementation (from production EAs)
double LotsByRisk(double riskPercent, double slPrice)
{
double riskMoney = AccountBalance() * riskPercent / 100.0;
// Guard against bad data
if (Bid == 0 || Ask == 0) return MarketInfo(Symbol(), MODE_MINLOT);
double pipValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double slPips = MathAbs((Ask - slPrice) / Point);
if (pipValue <= 0) pipValue = 1.0;
if (slPips <= 0) return MarketInfo(Symbol(), MODE_MINLOT);
double lots = riskMoney / (slPips * pipValue);
// Round down to broker's lot step (don't round up – that increases risk)
double step = MarketInfo(Symbol(), MODE_LOTSTEP);
lots = MathFloor(lots / step) * step;
// Clamp to broker limits
lots = MathMax(lots, MarketInfo(Symbol(), MODE_MINLOT));
lots = MathMin(lots, MarketInfo(Symbol(), MODE_MAXLOT));
return NormalizeDouble(lots, 2);
}Python Equivalent
def calculate_lot_size(account_balance, risk_pct, stop_distance_pips, pip_value):
"""
Calculate position size for consistent risk per trade.
Args:
account_balance: Current account balance
risk_pct: Risk percentage (e.g., 1.0 for 1%)
stop_distance_pips: Stop loss distance in pips
pip_value: Value of 1 pip for 1 standard lot
"""
risk_amount = account_balance * risk_pct / 100
lot_size = risk_amount / (stop_distance_pips * pip_value)
return round(lot_size, 2)
# Example: $10,000 account, 1% risk, 25 pip stop, $10/pip for EURUSD
lots = calculate_lot_size(10000, 1.0, 25, 10) # = 0.40 lotsThe formula: $\text{Lots} = \frac{\text{Balance} \times \text{Risk%} / 100}{\text{SL}_{\text{pips}} \times \text{PipValue}}$
At 1% risk per trade, you can be wrong fifty times consecutively and still have over 60% of your account left. That's not optimism. That's compound math working in your favor for once.
Technique 3: Maximum Drawdown Limit
Even strategies with a genuine edge go through ugly stretches. The question isn't will you have a drawdown. It's how bad do you let it get before the machine shuts itself off?
Track peak equity. When the gap between peak and current gets too wide, stop trading. Walk away. Let the market do its thing while your capital stays intact.
MQL4 Implementation
bool ExceededDrawdown(double maxDrawdownPercent)
{
static double peakEquity = 0.0;
double currentEquity = AccountEquity();
// Track the highest equity we've reached
if (peakEquity < currentEquity) peakEquity = currentEquity;
// Calculate current drawdown percentage
double drawdownPct = (peakEquity - currentEquity) / peakEquity * 100.0;
if (drawdownPct >= maxDrawdownPercent)
{
Print("DRAWDOWN LIMIT HIT: ", DoubleToString(drawdownPct, 2),
"% >= ", maxDrawdownPercent, "%. Trading paused.");
return true;
}
return false;
}
// Usage in OnTick():
void OnTick()
{
if (ExceededDrawdown(10.0)) return; // Stop trading at 10% drawdown
// ... rest of trading logic
}Python Equivalent
class DrawdownMonitor:
def __init__(self, max_drawdown_pct=10.0):
self.peak_equity = 0
self.max_dd_pct = max_drawdown_pct
def check(self, current_equity):
self.peak_equity = max(self.peak_equity, current_equity)
dd_pct = (self.peak_equity - current_equity) / self.peak_equity * 100
if dd_pct >= self.max_dd_pct:
print(f"DRAWDOWN LIMIT: {dd_pct:.2f}% – trading paused")
return False # Don't trade
return True # OK to tradeWe've seen people set this at 30%. Don't. By the time you're down 30%, the psychological damage alone makes recovery almost impossible – even if the math says otherwise. Five to ten percent for conservative strategies. Fifteen to twenty for aggressive ones.
Technique 4: Daily Trade Limit
Choppy markets are a bot's worst enemy. Whipsaw after whipsaw, your algorithm enters and exits and enters again, bleeding commissions and slippage with every round trip. We've watched EAs rack up 50+ trades in a single session during range-bound days. Not one of those trades needed to happen.
Put a cap on it.
MQL4 Implementation
int CountTradesToday()
{
int count = 0;
datetime startOfDay = iTime(NULL, PERIOD_D1, 0);
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderOpenTime() >= startOfDay) count++;
}
// Also check closed orders in history
for (int j = OrdersHistoryTotal() - 1; j >= 0; j--)
{
if (!OrderSelect(j, SELECT_BY_POS, MODE_HISTORY)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderOpenTime() >= startOfDay) count++;
}
return count;
}
// Usage:
if (CountTradesToday() >= MaxTradesPerDay) return; // Skip new entriesThree to five for swing strategies. Ten to twenty for scalpers. Beyond that, you'd better be running true HFT with sub-millisecond infrastructure – and if you are, you're probably not reading this article.
Technique 5: Spread Filter
Spreads widen at the worst possible moments – thin liquidity, news bombs, session transitions. If your stop loss is 10 pips and you enter with a 5-pip spread, congratulations: you started the trade already halfway to being stopped out.
MQL4 Implementation
bool IsSpreadAcceptable(double maxSpreadPoints)
{
double currentSpread = (Ask - Bid) / Point;
if (currentSpread > maxSpreadPoints)
{
Print("Spread too wide: ", DoubleToString(currentSpread, 1),
" > ", maxSpreadPoints, " – skipping trade");
return false;
}
return true;
}
// Usage:
if (!IsSpreadAcceptable(20.0)) return; // Max 20 points (2 pips on 5-digit broker)If your average stop is 30 pips, keep your max spread under 6. Twenty percent is the ceiling.
Technique 6: Trading Hours Filter
Not all hours are created equal. EUR pairs during the Asian session? Thin as paper. The last 15 minutes before US close? Chaos. Your strategy might have an edge – but only during London. Or only during the New York overlap. Code accordingly.
MQL4 Implementation
bool IsTradingHour(int startHour, int endHour)
{
int currentHour = TimeHour(TimeCurrent());
if (startHour <= endHour)
return (currentHour >= startHour && currentHour <= endHour);
else // Handles overnight windows (e.g., 22 to 06)
return (currentHour >= startHour || currentHour <= endHour);
}
bool IsAllowedWeekday(bool allowMonday, bool allowFriday)
{
int weekday = TimeDayOfWeek(TimeCurrent()); // 0=Sunday
if (weekday == 0) return false; // Never trade Sunday
if (weekday == 1 && !allowMonday) return false;
if (weekday == 5 && !allowFriday) return false;
return true;
}Monday opens carry weekend gap risk. Friday trades carry over-the-weekend exposure. Both are optional hazards. Every production EA we build lets you toggle them off.
Technique 7: Trailing Stop Losses
Here's a scenario that hurts every time: you catch a real move. Price runs 200 pips in your direction. But your take profit was set at 50 pips because that's what the backtest said was optimal. Or worse – price runs 150 pips, you don't take profit, and it reverses all the way back to your entry.
Trailing stops fix both problems. Lock in profit as price moves. Let winners breathe. Don't give it all back.
MQL4 Implementation (from production code)
void ManageTrailingStops(int trailStartPoints, int trailStepPoints)
{
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderSymbol() != Symbol()) continue;
double profitPoints;
if (OrderType() == OP_BUY)
profitPoints = (Bid - OrderOpenPrice()) / Point;
else if (OrderType() == OP_SELL)
profitPoints = (OrderOpenPrice() - Ask) / Point;
else continue;
// Only start trailing after minimum profit reached
if (profitPoints < trailStartPoints) continue;
double newSL;
if (OrderType() == OP_BUY)
{
newSL = NormalizeDouble(Bid - trailStepPoints * Point, Digits);
// Only move stop UP, never down
if (newSL > OrderStopLoss())
OrderModify(OrderTicket(), OrderOpenPrice(), newSL,
OrderTakeProfit(), 0, clrBlue);
}
else // OP_SELL
{
newSL = NormalizeDouble(Ask + trailStepPoints * Point, Digits);
// Only move stop DOWN, never up
if (newSL < OrderStopLoss() || OrderStopLoss() == 0)
OrderModify(OrderTicket(), OrderOpenPrice(), newSL,
OrderTakeProfit(), 0, clrRed);
}
}
}Start trailing after 30 pips of profit, keep the stop 5 pips behind. Adjust to taste and your strategy's ATR.
Technique 8: Minimum Free Margin Check
Opening positions near margin call is like gunning the engine with the oil light on. The broker will liquidate you at the worst possible price. Check free margin before every entry. We use a 2× buffer – if you can't afford twice the margin required, sit this one out.
MQL4 Implementation
bool HasSufficientMargin(double lots)
{
double freeMargin = AccountFreeMargin();
double requiredMargin = MarketInfo(Symbol(), MODE_MARGINREQUIRED) * lots;
// Require at least 2× the margin needed (safety buffer)
if (freeMargin < requiredMargin * 2)
{
Print("Insufficient margin. Free: ", freeMargin,
" Required: ", requiredMargin);
return false;
}
return true;
}Technique 9: Equity-Based Exit (The Nuclear Option)
Individual stops protect individual trades. But what about correlated losses across five positions? Or a flash crash that gaps through your stops entirely?
This is the emergency brake. Monitor total account equity. If it drops below a floor – or hits a daily profit target – flatten everything. No exceptions.
Nearly every professional EA we've built uses some variation of this:
void CheckEquityLimits()
{
double balance = AccountBalance();
double equity = AccountEquity();
// Emergency exit: close everything if equity drops too far
double maxLoss = balance * 0.10; // 10% of balance
if (equity < balance - maxLoss)
{
Print("EQUITY EMERGENCY: Closing all positions");
CloseAllOrders();
return;
}
// Profit target: lock in gains
double profitTarget = balance * 0.05; // 5% profit target
if (equity > balance + profitTarget)
{
Print("PROFIT TARGET HIT: Closing all positions");
CloseAllOrders();
}
}Technique 10: Maximum Concurrent Positions
Strong trends generate signal after signal. Without a cap, your bot piles in – five trades, ten trades, twenty – all pointing the same direction. And when the reversal comes, it takes the entire stack down at once.
Three to five concurrent positions is a sane default. More than that and you're not diversifying; you're concentrating.
MQL4 Implementation
int CountOpenPositions()
{
int count = 0;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderType() == OP_BUY || OrderType() == OP_SELL) count++;
}
return count;
}
// Usage in trade entry:
if (CountOpenPositions() >= MaxPositions) return; // Don't stack tradesThe Master Risk Check Function
In production, we collapse all ten checks into a single gatekeeper. One function call before any entry logic. If it returns false, the EA goes back to sleep. Clean. Simple. Saves accounts.
bool PassesRiskChecks()
{
// 1. Drawdown check
if (ExceededDrawdown(10.0)) return false;
// 2. Daily trade limit
if (CountTradesToday() >= MaxTradesPerDay) return false;
// 3. Spread filter
if (!IsSpreadAcceptable(20.0)) return false;
// 4. Trading hours
if (!IsTradingHour(TradingStartHour, TradingEndHour)) return false;
// 5. Weekday filter
if (!IsAllowedWeekday(AllowMonday, AllowFriday)) return false;
// 6. Margin check
if (AccountFreeMargin() < MinimumFreeMargin) return false;
// 7. Max positions
if (CountOpenPositions() >= MaxPositions) return false;
return true; // All checks passed – safe to trade
}One line. All ten checks. That's the whole point – building it once so you never have to think about it again.
void OnTick()
{
if (!PassesRiskChecks()) return; // One line. All risk checks. Done.
// ... entry logic
}Risk Management Cheat Sheet
| Technique | Prevents | Recommended Default |
|---|---|---|
| ATR Stop Loss | Stops too tight/wide for conditions | 1.5 × ATR(14) |
| Risk-Based Sizing | Inconsistent risk per trade | 1-2% per trade |
| Max Drawdown | Account devastation | 10% pause, 20% stop |
| Daily Trade Limit | Overtrading in chop | 3-5 (swing), 10-20 (scalp) |
| Spread Filter | Bad entries in thin markets | < 20% of avg stop distance |
| Trading Hours | Low-liquidity traps | Active sessions only |
| Trailing Stops | Giving back winners | Start at 300pts, trail at 50pts |
| Margin Check | Margin calls | 2× required margin minimum |
| Equity Exit | Catastrophic multi-position loss | ±5-10% of balance |
| Max Positions | Concentration risk | 3-5 concurrent positions |
Need Someone to Audit Your Risk Logic?
Every algorithm we build ships with all ten of these baked in. Not because we're cautious by nature – because we've seen what happens without them.


