Edgoro
Risk

MQL5 lot size from risk percent: a sizing function that works on any symbol

Calculate MT5 lot size from a risk percent in MQL5 with OrderCalcProfit: round to the volume step, include commission, check margin, avoid oversized trades.

"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
InputWhere it comes from
Money at riskEquity (or balance) × risk %
Loss per lotDistance from entry to stop, converted to account currency for 1 lot
Volume limitsThe 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

MethodHowWatch out for
Tick valueStop distance ÷ tick size × tick valueTick 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 stopNeeds 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

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.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro