How to set the correct lot size from risk percent in MQL5
Risk-based position sizing looks like a two-line problem until a live account disagrees with your spreadsheet. The usual lots = risk / stop shortcut assumes every symbol prices a point the same way, which falls apart on gold, index CFDs, and anything quoted in a currency that is not your account currency. Below is the version that holds up, plus the MQL5 function I use for it.
Why the obvious formula breaks
The tempting approach is to hardcode "one standard lot moves $10 per pip" and divide. That is roughly true on EURUSD and wrong everywhere else. On USDJPY at 150.00, one pip per standard lot is worth about $6.67, not $10, because a pip is 0.01 JPY and you have to convert back to dollars. On gold, on the DAX, on crypto CFDs, the contract size is different again, and the account currency conversion changes with every quote.
You do not need to work any of that out yourself. The terminal already publishes the money value of one tick, converted into your deposit currency, via SYMBOL_TRADE_TICK_VALUE_LOSS. Pair it with SYMBOL_TRADE_TICK_SIZE and the arithmetic becomes symbol-agnostic.
The formula
risk money = account value × risk percent / 100
loss per lot = (stop distance in price / tick size) × tick value
raw lots = risk money / loss per lot
Then round *down* to the broker's volume step. Rounding up is how people accidentally exceed their risk limit by 30% on small accounts.
Step by step
1. Choose your base. Balance is stable and predictable. Equity is more honest, because it shrinks during drawdown and your position sizes shrink with it. I default to equity for live trading and balance for fixed-fractional testing.
2. Get the stop distance in price, not points. If you think in points, multiply by SYMBOL_POINT. Working in price units avoids the 4-digit versus 5-digit trap entirely.
3. Read tick size and tick value. Ask for SYMBOL_TRADE_TICK_VALUE_LOSS first and fall back to SYMBOL_TRADE_TICK_VALUE. Some brokers leave the loss variant at zero.
4. Compute the loss per lot. Divide the stop distance by the tick size to get the number of ticks, then multiply by the tick value. That is what one lot costs you if the stop is hit.
5. Divide risk money by loss per lot. This is your raw, unrounded size.
6. Round down and clamp. Apply SYMBOL_VOLUME_STEP, then reject anything below SYMBOL_VOLUME_MIN and cap at SYMBOL_VOLUME_MAX.
7. Sanity-check the stop against SYMBOL_TRADE_STOPS_LEVEL. If your stop is closer than the broker minimum, the order is rejected regardless of how good your maths is.
The code
Drop this into any EA. It compiles as a standalone script as written.
//+------------------------------------------------------------------+
//| LotFromRisk.mq5 |
//| Position size from a fixed account risk percentage. |
//+------------------------------------------------------------------+
#property script_show_inputs
input double InpRiskPercent = 1.0; // Risk per trade, % of account
input double InpStopLossPts = 200; // Stop loss in points
//+------------------------------------------------------------------+
//| Lot size that risks `riskPercent` of `balance` if price moves |
//| `stopDistance` against the position. Returns 0.0 when the trade |
//| cannot be sized inside the symbol's volume limits. |
//+------------------------------------------------------------------+
double LotSizeFromRisk(const string symbol,
const double accountValue,
const double riskPercent,
const double stopDistance)
{
if(riskPercent <= 0.0 || accountValue <= 0.0 || stopDistance <= 0.0)
return(0.0);
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE_LOSS);
if(tickValue <= 0.0)
tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
// Last resort: derive from contract size. Correct for USD-quoted
// forex only, so treat it as a guard rather than a real answer.
if(tickSize <= 0.0 || tickValue <= 0.0)
{
tickSize = SymbolInfoDouble(symbol, SYMBOL_POINT);
tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE) * tickSize;
}
if(tickSize <= 0.0 || tickValue <= 0.0)
return(0.0);
double riskMoney = accountValue * riskPercent / 100.0;
double lossPerLot = (stopDistance / tickSize) * tickValue;
if(lossPerLot <= 0.0)
return(0.0);
double lots = riskMoney / lossPerLot;
double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
if(step <= 0.0)
step = 0.01;
// Round DOWN. Rounding up would breach the risk limit.
lots = MathFloor(lots / step) * step;
lots = NormalizeDouble(lots, 8);
if(lots < minLot)
return(0.0);
if(lots > maxLot)
lots = maxLot;
return(lots);
}
//+------------------------------------------------------------------+
void OnStart()
{
string sym = _Symbol;
double accountValue = AccountInfoDouble(ACCOUNT_EQUITY);
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
double stopDistance = InpStopLossPts * point;
int stopLevel = (int)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
if(stopLevel > 0 && InpStopLossPts < stopLevel)
{
PrintFormat("Stop of %.0f points is inside the broker minimum of %d.",
InpStopLossPts, stopLevel);
return;
}
double lots = LotSizeFromRisk(sym, accountValue, InpRiskPercent, stopDistance);
if(lots <= 0.0)
{
PrintFormat("Risk of %.2f%% on %.2f is below the minimum lot for %s.",
InpRiskPercent, accountValue, sym);
return;
}
PrintFormat("%s: %.2f%% of %.2f = %.2f risk, SL %.0f pts -> %.2f lots",
sym, InpRiskPercent, accountValue,
accountValue * InpRiskPercent / 100.0, InpStopLossPts, lots);
}
What the calculation still misses
Three things, and they matter more on small accounts than large ones.
Commission and swap. The tick value covers price movement only. If your broker charges $7 per lot round turn, that is real risk sitting outside the stop. On a 0.01 lot trade with a $10 stop, commission can be 10% of your intended risk. Add the per-lot commission to lossPerLot if you want to be strict.
Spread. Sizing from the entry price ignores the fact that a sell stops out at the ask. On a 2-pip spread with a 20-pip stop, that is a 10% error in the wrong direction.
Gaps and slippage. A stop is an instruction, not a guarantee. Nothing in the sizing maths protects you from a weekend gap through your level.
Where to test it
Sizing logic is easy to unit-test but impossible to trust until you have watched it trade. Print the computed lot, the risk money, and the actual closed P&L per position, and compare them across a few hundred trades. If the average loss is consistently 5-8% above target, commission and spread are your culprits. If you would rather look at a finished example first, [see a real backtest](https://mql.ranartech.com/r/IxZSfChQV6Fn) — EURUSD H1 EA (+$340, 73% win, 26 trades) — and note how few trades it takes before a sizing error compounds into a visible drawdown.
Get the function right once, and every strategy you write afterwards inherits a risk profile you can actually reason about.
Build & backtest your own EA free →