"Risk 1% per trade" is easy to say and surprisingly easy to get wrong in code. A hard-coded lot size ignores the stop distance. A formula that works on EUR/USD can be off by a factor of a hundred on gold or an index CFD. And a careless rounding step can quietly turn 1% into 1.5%.
This article builds a lot-size function for MetaTrader 5 that starts from a risk percentage and a stop price, works across forex, metals, indices and crypto CFDs, rounds safely, includes commission and checks margin. It's an example to adapt and test, not financial advice.
The formula
lots = money you accept losing ÷ money lost per 1 lot if the stop is hit
| Input | Where it comes from |
|---|---|
| Money at risk | Equity (or balance) × risk % |
| Loss per lot | Distance from entry to stop, converted to account currency for 1 lot |
| Volume limits | The symbol's minimum, maximum and step |
Everything difficult is in the second line. "Loss per lot" depends on the contract size, the tick size and value, and the conversion from the instrument's profit currency to your account currency.
Two ways to get the loss per lot
| Method | How | Watch out for |
|---|---|---|
| Tick value | Stop distance ÷ tick size × tick value | Tick value is quoted for the current price; for cross pairs it changes as rates move |
OrderCalcProfit() | Ask the terminal for the profit of 1 lot from entry to stop | Needs a valid symbol and prices; doesn't include commission |
The MQL5 Expert Advisor risk checklist uses the tick-value method. Here we use OrderCalcProfit(): the terminal does the currency conversion with the same logic it uses for real trades, which removes a whole class of mistakes.
The function
#include <Trade/Trade.mqh>
input double InpRiskPercent = 1.0; // Risk per trade, % of equity
input double InpCommissionPerLot = 0.0; // Round-turn commission per lot, account currency
input int InpStopPoints = 250; // Example stop distance in points (for the demo in OnInit)
// Digits needed to express a volume step: 0.01 -> 2, 0.1 -> 1, 1 -> 0.
int VolumeDigits(const double step)
{
int digits = 0;
double s = step;
while(digits < 8 && MathAbs(s - MathRound(s)) > 1e-9)
{
s *= 10.0;
digits++;
}
return digits;
}
// Volume that loses riskPercent of equity if price goes from entry to sl. Returns 0 when the trade can't respect it.
double LotsForRisk(const string symbol, const ENUM_ORDER_TYPE type, const double entry, const double sl, const double riskPercent)
{
if(riskPercent <= 0.0 || entry <= 0.0 || sl <= 0.0 || entry == sl)
return 0.0;
double riskMoney = AccountInfoDouble(ACCOUNT_EQUITY) * riskPercent / 100.0;
// Profit of 1 lot from entry to stop, in account currency: negative when the stop is on the correct side.
double pnlOneLot = 0.0;
if(!OrderCalcProfit(type, symbol, 1.0, entry, sl, pnlOneLot) || pnlOneLot >= 0.0)
return 0.0;
double lossOneLot = -pnlOneLot + InpCommissionPerLot;
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)
return 0.0;
// Round DOWN to the volume step; the epsilon stops 0.3 / 0.1 = 2.9999... from losing a whole step.
double lots = MathFloor(riskMoney / lossOneLot / step + 1e-9) * step;
lots = NormalizeDouble(lots, VolumeDigits(step));
if(lots < minLot)
return 0.0; // even the minimum volume would risk too much: skip the trade
lots = MathMin(lots, maxLot);
// Enough free margin for this volume?
double margin = 0.0;
if(!OrderCalcMargin(type, symbol, lots, entry, margin) || margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE))
return 0.0;
return lots;
}
int OnInit()
{
// Demo: size a buy at the current ask with a stop InpStopPoints below it, and print the result.
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = NormalizeDouble(ask - InpStopPoints * _Point, _Digits);
double lots = LotsForRisk(_Symbol, ORDER_TYPE_BUY, ask, sl, InpRiskPercent);
int vd = VolumeDigits(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP));
PrintFormat("%s: risk %.2f%% with a %d-point stop -> %s lots", _Symbol, InpRiskPercent, InpStopPoints, DoubleToString(lots, vd));
return INIT_SUCCEEDED;
}
void OnTick()
{
}
Walking through it
Equity, not balance. Sizing from equity means open losses reduce the size of the next trade. Some traders prefer balance; whichever you choose, use it consistently.
The sign check. For a buy with the stop below entry, OrderCalcProfit() returns a negative number. If it returns zero or a positive number, the stop is on the wrong side or the call failed, and the function refuses to size the trade rather than guessing.
Rounding down. Volumes must be a multiple of the symbol's step. Rounding to the nearest step can round up and exceed your risk; MathFloor always rounds down. Floating-point division can produce 2.9999999 instead of 3, so a tiny epsilon is added before flooring, and the result is normalised to the step's number of decimals.
The minimum volume. If the risk-based size is below the broker's minimum, the function returns zero. The tempting alternative, trading the minimum anyway, is how a "1% risk" EA ends up risking several percent on a small account.
Margin. A trade can be correctly sized for risk and still be rejected for lack of margin, especially with tight stops or low leverage. OrderCalcMargin() checks first.
Sells, pending orders and other symbols
The same function covers the other cases without changes:
- Sells: pass
ORDER_TYPE_SELL, the bid as the entry and a stop above it.OrderCalcProfit()still returns a negative number for a correctly placed stop. - Pending orders: pass the order's price as the entry, not the current price, and size the order again whenever you move it.
- Other symbols: an EA that trades a basket can pass any symbol name. Volume limits and currency conversion are read for that symbol. Make sure it's in Market Watch first (
SymbolSelect(name, true)), otherwise its prices may not be available yet.
If the function returns zero, log why. "Stop on the wrong side", "below the minimum volume" and "not enough margin" call for very different fixes, and a silent zero hides which one happened.
A worked example
Equity is $10,000 and risk is 1%, so the money at risk is $100. A buy on EUR/USD with a stop 250 points (25 pips) below entry, on a standard 100,000-unit contract in a USD account, loses about $250 per lot.
- Without commission: 100 ÷ 250 = 0.40 lots.
- With a hypothetical $7 round-turn commission: 100 ÷ 257 = 0.389, rounded down to 0.38 lots.
The same function on gold, an index or a JPY pair returns different volumes for the same risk, because the terminal converts each one correctly. Check your broker's contract specifications, and compare a few results with the position size calculator.
Costs the function doesn't see
- Spread. A buy fills at the ask and its stop triggers on the bid. Measure the stop distance from the actual entry price, and remember that spread widening near the stop can trigger it earlier.
- Slippage. Stops become market orders. In fast markets the fill can be worse than the stop price, so the realised loss can exceed the plan.
- Swaps. Positions held overnight pay or receive swap, which isn't part of the stop calculation.
This is why many traders keep their risk per trade comfortably below their hard limits, especially on funded accounts with a daily loss rule; see coding a prop-firm daily loss guard.
Using it in an EA
Compute the stop first, from your strategy (an ATR multiple, a swing low, a fixed distance), then call LotsForRisk() with the real entry price, and skip the trade when it returns zero. Send every order with its stop attached. The background on choosing a percentage is in position sizing and the 1% rule.
Sources and further reading
- MQL5 Reference: OrderCalcProfit() — the profit or loss of a hypothetical trade in the account currency.
- MQL5 Reference: OrderCalcMargin() — the margin a trade would need.
- MQL5 Reference: Symbol properties —
SYMBOL_VOLUME_MIN,SYMBOL_VOLUME_MAXandSYMBOL_VOLUME_STEP. - MQL5 Reference: AccountInfoDouble() — equity and free margin.
Try it with Edgoro
Ask Edgoro for an MT5 Expert Advisor that "risks 0.75% of equity per trade with the stop at 1.5 × ATR(14), includes $7 commission per lot in the sizing, and skips trades when the minimum lot would exceed the risk". You get MQL5 code with the sizing built in, ready for the Strategy Tester. Generate your first EA.
Position sizing limits risk; it doesn't remove it. Test every EA on a demo account first. Trading involves substantial risk of loss and nothing here is investment advice.