Risk Management in MQL5: Position Sizing and Broker-Valid Stops

Forex trader at a dark multi-monitor desk with candlestick charts and MQL5 code on screen

Technical revision: 1 October 2026 · Educational material

Position sizing should translate an explicit loss estimate into a broker-valid volume. It should also know when no valid order fits the budget. A stop defines a requested exit condition; it cannot guarantee the realized loss when execution gaps or costs differ from the estimate.

Estimate entry-to-stop loss in account currency

Using a fixed pip value can fail across symbols, contract types and account currencies. The helper uses OrderCalcProfit at a valid reference volume, then scales the estimated adverse result per lot. It adds a user-supplied allowance for costs in account currency per lot.

For a buy, the stop must be below entry; for a sell, above entry. Treat an invalid direction, unavailable calculation or non-adverse estimate as a reason to skip the order. Estimate from the proposed entry quote, which is normally ask for a market buy and bid for a market sell.

Round down and reject oversize minimums

Read SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX and SYMBOL_VOLUME_STEP. Two decimal places are not universal: a step can be 0.001 or a larger increment. Rounding up or clamping to the minimum may exceed the requested risk.

Hypothetical example: a budget of 25 account-currency units and estimated loss of 1,000 per lot permits 0.025 lots before costs. With a 0.01 step, 0.02 lots fits; 0.03 does not. If the minimum is 0.10 lots, skip the trade. Do not force the minimum.

#ifndef FTS_RISK_MQH
#define FTS_RISK_MQH
// Educational helper. Estimates exclude gaps and slippage.
int FtsVolumeDigits(const double step)
{
   for(int digits=0; digits<=8; digits++)
      if(MathAbs(step-NormalizeDouble(step,digits))<1e-10) return digits;
   return 8;
}
double FtsVolumeForRisk(const string symbol,const ENUM_ORDER_TYPE side,
                       const double entry,const double stop,
                       const double risk_money,const double cost_per_lot=0.0)
{
   if(risk_money<=0.0 || cost_per_lot<0.0 || entry<=0.0 || stop<=0.0) return 0.0;
   if((side==ORDER_TYPE_BUY && stop>=entry) ||
      (side==ORDER_TYPE_SELL && stop<=entry) ||
      (side!=ORDER_TYPE_BUY && side!=ORDER_TYPE_SELL)) return 0.0;
   double minimum=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
   double maximum=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX);
   double step=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
   if(minimum<=0.0 || maximum<minimum || step<=0.0) return 0.0;
   double profit=0.0;
   if(!OrderCalcProfit(side,symbol,minimum,entry,stop,profit) || profit>=0.0)
      return 0.0;
   double loss_per_lot=(-profit/minimum)+cost_per_lot;
   if(loss_per_lot<=0.0 || !MathIsValidNumber(loss_per_lot)) return 0.0;
   double raw=MathMin(risk_money/loss_per_lot,maximum);
   if(raw<minimum) return 0.0; // NEVER raise volume to the minimum.
   double volume=MathFloor(raw/step)*step;
   volume=NormalizeDouble(volume,FtsVolumeDigits(step));
   if(volume<minimum || volume>maximum || volume<=0.0) return 0.0;
   double rounded_profit=0.0;
   if(!OrderCalcProfit(side,symbol,volume,entry,stop,rounded_profit)) return 0.0;
   double estimated_loss=-rounded_profit+volume*cost_per_lot;
   if(estimated_loss<=0.0 || estimated_loss>risk_money+1e-8) return 0.0;
   return volume;
}
double FtsRoundPrice(const string symbol,const double price,const bool down)
{
   double tick_size=SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_SIZE);
   int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
   if(tick_size<=0.0) return 0.0;
   double ticks=price/tick_size;
   return NormalizeDouble((down ? MathFloor(ticks) : MathCeil(ticks))*tick_size,digits);
}
#endif

Round prices on the tick grid

Displayed digits describe price formatting, while SYMBOL_TRADE_TICK_SIZE describes the permitted price increment. The helper includes directional rounding on that grid. For a buy stop below market, round downward; for a sell stop above market, upward. Recalculate size using the final rounded stop, because it changes the loss estimate.

Then check the stop against the live quote and the reported stops level. A modification can additionally be constrained by the freeze level. Refresh the quote before a request and inspect the server result afterward; passing a local check does not guarantee acceptance.

Account risk is broader than one stop

Equity-based sizing responds to current account equity; balance-based sizing does not include unrealized results. Neither removes the need to consider simultaneous exposures, margin usage, correlated positions and execution costs. Do not interpret a collection of separately sized orders as a portfolio loss cap.

The demo EA in the package checks estimated margin and blocks existing symbol exposure. It sends the initial stop and target together with the request. A broker rejection is logged, rather than resolved by deliberately opening without protection.

Test small budgets, nonstandard volume steps and changing symbol specifications. Follow the order-error guide when the broker rejects volume or stops, and use the validation checklist to record the assumptions.

References: OrderCalcProfit, symbol properties.

By Forex Real Trader

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like