Almost all literature on algorithmic position sizing stops at the standard formula:
lots = (balance * risk_percent) / (stop_distance * tick_value_per_lot)
That part is straightforward. The subtle failure does not live in the equation itself: it hides in the three lines that follow, where the raw lot size is quantized to the broker volume step (lotStep) and bounded by the broker minimum lot size (minLot).
We found this exact silent bug in our own production API library. We document it here because the pattern is ubiquitous across MQL5 codebase repositories, open-source python engines, and commercial indicators—and because the exposure concentration lands squarely on small trading accounts.
1. The Bug Pattern: MathMax(minLot, lots)
Consider the canonical closing block of a position sizing routine found across MetaTrader 5 forums and GitHub repositories:
double lots = MathFloor(rawLot / lotStep) * lotStep;
return MathMax(minLot, MathMin(maxLot, lots)); // <-- The Silent Failure
Using MathFloor is sound: rounding down guarantees you never exceed the calculated risk ceiling.
The structural flaw is MathMax(minLot, ...).
When the budgeted lot size falls below the broker required minimum (e.g. rawLot = 0.005 vs minLot = 0.01), MathFloor rounds down to 0.00. MathMax subsequently intercepts this value and clamps it upward to 0.01. The function returns an executable order size without emitting an error or warning state.
That position no longer adheres to the risk budget. Crucially, the execution pipeline continues reporting the original theoretical budget as if it were the actual risk carried.
2. Empirical Measurement: A 100% Risk Overshoot
Consider a typical micro account scenario:
- Account Balance: $500 USD
- Target Risk: 0.5% ($2.50 USD budget)
- Stop Loss Distance: 500 ticks (50 pips on EURUSD)
- Tick Value per Standard Lot: $1.00 USD per tick
| Metric | Measured Value |
| Budgeted Risk (0.5% of $500) | $2.50 USD |
| Raw Unrounded Lots | 0.005 lots |
MathFloor quantized to 0.01 step | 0.00 lots |
Returned after MathMax(minLot, ...) | 0.01 lots |
| Effective Realized Risk of 0.01 lots | $5.00 USD |
| Reported Risk by Legacy Engine | $2.50 USD |
The effective exposure is exactly double (100% overshoot) the budgeted parameter, and nothing in the execution response warns the operator.
This is not a degenerate edge case: it is a $500 account trading with conservative risk parameters. This profile represents the exact retail trader who can least afford hidden risk multiplication.
The Problem With round() vs floor()
A common alternative in algorithmic code is using round() rather than floor(). We ran a brute-force sweep across a grid of balances ($500–$20,000 USD), stop loss distances (5–120 pips), and risk budgets (0.5%, 1%, 2%):
| Volume Step | Worst Measured Overshoot | Parameter Combination |
| 0.01 | +99.15% | Balance $1,175 USD, Risk 0.5%, Stop 117 pips |
| 0.10 | +99.58% | Balance $12,025 USD, Risk 0.5%, Stop 120 pips |
Whenever the unrounded lot size lands just above the midpoint of the step threshold (e.g., 0.00501), round() jumps to the higher tier, nearly doubling effective exposure.
Hardcoded Pip Values: The Inverse Failure
Another common vulnerability is hardcoding pip_value = 10.0, which only holds for standard forex contracts with USD as the quote currency. On Gold (XAUUSD), the tick value per 1 lot is $1.00 USD per 10 points.
For an account with $10,000 USD balance, 1% risk ($100 budget), and a 320-point stop loss on XAUUSD:
- Assuming $10/pip: raw lots
0.0312 -> actual risk $9.60 USD. The algorithm risks less than a tenth of the intended allocation, leaving the strategy structurally under-allocated.
- Using live symbol specification ($1/point): raw lots
0.3125 -> actual risk $99.20 USD.
3. The Quantitative Fix
The invariant is clean: if the budgeted lot size cannot satisfy the broker minimum contract threshold, the trade does not fit within the defined risk constraints. Arbitrarily inflating the volume to minLot is an unbudgeted contract change.
The sizing function must either reject the trade or explicitly return the effective clamped exposure alongside a boolean violation flag:
Python Implementation
import math
def calculate_position_size(balance: float, risk_pct: float, sl_ticks: float,
tick_value: float, lot_step: float = 0.01,
min_lot: float = 0.01, max_lot: float = 100.0) -> dict:
budget = balance * (risk_pct / 100.0)
loss_per_lot = sl_ticks * tick_value
if loss_per_lot <= 0:
return {"executable": False, "reason": "Invalid stop loss distance or tick value"}
raw_lots = budget / loss_per_lot
quantized_lots = math.floor(raw_lots / lot_step) * lot_step
if quantized_lots < min_lot:
effective_risk = min_lot * loss_per_lot
return {
"executable": False,
"reason": "Budgeted lot size is below broker minimum contract size",
"raw_lots": round(raw_lots, 6),
"budget_usd": round(budget, 2),
"min_lot_required": min_lot,
"effective_risk_if_forced_usd": round(effective_risk, 2),
"risk_overshoot_pct": round(((effective_risk / budget) - 1.0) * 100.0, 2)
}
clamped_lots = min(quantized_lots, max_lot)
actual_risk = clamped_lots * loss_per_lot
return {
"executable": True,
"lots": round(clamped_lots, 2),
"budget_usd": round(budget, 2),
"actual_risk_usd": round(actual_risk, 2),
"clamped_to_max": quantized_lots > max_lot
}
MQL5 Production Guard
In automated expert advisors (EAs), do not mask the floor violation:
double lots = MathFloor(rawLot / lotStep) * lotStep;
// If the budgeted lot size does not reach the broker minimum,
// the trade CANNOT be placed without violating risk limits.
if(lots < minLot)
{
PrintFormat("[RISK ENGINE ERROR] Budgeted volume %.4f < minLot %.2f. Order rejected.", rawLot, minLot);
return 0.0;
}
return MathMin(maxLot, lots);
4. Production Engineering Checklist
- Strictly
MathFloor, never MathRound: Midpoint rounding yields up to 100% risk overshoot on fractional lots.
- Never clamp to
minLot silently: Reject or return explicit clampedToMin metadata with effective USD risk.
- Query dynamic tick properties: Always poll
SYMBOL_TRADE_TICK_VALUE and SYMBOL_TRADE_TICK_SIZE via the broker API.
- Independent verification: Test your risk library directly, not just UI presentations where browser validation might mask server-side assumptions.
Reference Implementation
The complete open-source position sizer engine, mathematical documentation, and verified MQL5 indicators under AGPLv3 are available at:
MetaTrader 5 Algorithmic Position Sizer — GuetaQuant
Author: Mahdi Goodarzi (Google Developer Profile) · Founder & Quantitative Architect at Gueta Quant.