How to Calculate Lot Size by Risk Percentage in MQL5 (EA Code + Real Backtest)
To calculate lot size by risk percentage in MQL5, take the money you are willing to lose (balance × risk %), divide it by the loss one full lot would take between entry and stop loss (get that from OrderCalcProfit), then round the result down to the symbol's SYMBOL_VOLUME_STEP and clamp it between SYMBOL_VOLUME_MIN and SYMBOL_VOLUME_MAX. If the rounded volume is below the minimum, skip the trade.
Why fixed lots are a problem
Most beginner EAs trade a hard-coded 0.10 lots. That means a 200-point stop and a 900-point stop risk very different amounts of money, and the risk does not shrink when the account shrinks. Risk-based sizing fixes both: every trade risks the same share of the account, whatever the stop distance.
The usual hand-rolled formula is lots = risk / (stopPoints × tickValue). It works on EURUSD and quietly breaks elsewhere: tick value is quoted per tick size, not per point (gold, indices and many CFDs have a tick size larger than _Point), and on cross pairs the tick value moves with the exchange rate. You can do it correctly with SYMBOL_TRADE_TICK_VALUE_LOSS and SYMBOL_TRADE_TICK_SIZE, but there is a simpler route.
Let the terminal do the money maths: OrderCalcProfit
OrderCalcProfit(action, symbol, volume, price_open, price_close, profit) returns, in account currency, the profit of a trade from price_open to price_close. Ask it for 1.0 lot from your entry to your stop and you get the exact loss per lot, with contract size, tick size and currency conversion already handled. The value is negative for a losing move, so take MathAbs.
// Lots so that hitting the stop loses InpRiskPct % of balance.
double CalcLots(ENUM_ORDER_TYPE type, double entry, double sl)
{
if(sl <= 0.0 || InpRiskPct <= 0.0)
return(0.0);
double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPct / 100.0;
double lossPerLot = 0.0;
if(!OrderCalcProfit(type, _Symbol, 1.0, entry, sl, lossPerLot) || lossPerLot >= 0.0)
{
Print("OrderCalcProfit failed, err=", GetLastError());
return(0.0);
}
double lots = riskMoney / MathAbs(lossPerLot);
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
lots = MathFloor(lots / step) * step; // round DOWN so risk never exceeds target
if(lots < vmin)
return(0.0); // stop too wide for this balance: skip trade
return(NormalizeDouble(MathMin(lots, vmax), 2));
}
Three details matter here:
- Round down, never to nearest.
MathFloor(lots / step) * stepguarantees the real risk is at or below your target. Rounding to nearest can push you over it. - Respect the broker limits. Below
SYMBOL_VOLUME_MINthe order would be rejected (or you would have to over-risk by using the minimum). Returning 0 and skipping the trade is the honest choice. - Pass the real entry and SL prices. Use Ask for buys and Bid for sells, the same prices you send with the order, so the spread is part of the calculation.
Using it in an EA
Compute the stop first, then the volume, then send the order:
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = NormalizeDouble(ask - InpStopLossPts * _Point, _Digits);
double tp = NormalizeDouble(ask + InpTakeProfPts * _Point, _Digits);
double lots = CalcLots(ORDER_TYPE_BUY, ask, sl);
if(lots > 0.0)
c_Trade.Buy(lots, _Symbol, ask, sl, tp);
InpRiskPct is an input (1.0 = 1% per trade). Use ACCOUNT_BALANCE if you want risk to stay constant while trades are open; switch to ACCOUNT_EQUITY if you want it to shrink with floating losses. One limitation: NormalizeDouble(..., 2) assumes a volume step of 0.01 or coarser, which covers almost every forex and CFD symbol; if your broker uses 0.001 steps, normalise to 3 digits instead.
Real backtest of the example EA
We dropped CalcLots into a simple RSI(14) reversal EA (buy on a cross up through 30, sell on a cross down through 70, exit at 50, 500-point SL, 900-point TP, 1% risk per trade) and ran it in the MetaTrader 5 Strategy Tester:
- Symbol / timeframe: EURUSD H1
- Period: 2024.01.01 to 2024.07.01, $10,000 deposit
- Net profit: +$865
- Win rate: 70%
- Profit factor: 2.01
- Max drawdown: 2.0%
- Trades: 61
The point of the test is the sizing, not the signal: with a 1% risk cap the drawdown stays small even across losing streaks. Six months on one pair is a tiny sample and says nothing about other periods or symbols. Past performance does not guarantee future results.
Skip the boilerplate
If you would rather describe the rule than write it, Ranar Algo takes a plain-English strategy ("RSI 14 reversal on EURUSD H1, risk 1% per trade, 500-point stop"), generates the MQL5 Expert Advisor, compiles it, and runs a real MT5 backtest so you see the numbers before you trust the code. You get the source and you keep your IP. It is still improving, so feedback on anything it gets wrong is welcome.
Summary
Risk-based lot sizing in MQL5 is four steps: risk money from balance, loss per lot from OrderCalcProfit, round down to the volume step, clamp to min/max and skip if too small. It is a few lines, it works on every symbol your broker offers, and it is the single change that does most to keep an EA's drawdown under control.