The Silent Position Sizer Bug: When Accounts Are Small, Your Risk Inadvertently Doubles

Leader 1 2 14
calendar_todayschedule2 min read
— Originally published at guetaquant.com

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

  1. Strictly MathFloor, never MathRound: Midpoint rounding yields up to 100% risk overshoot on fractional lots.
  2. Never clamp to minLot silently: Reject or return explicit clampedToMin metadata with effective USD risk.
  3. Query dynamic tick properties: Always poll SYMBOL_TRADE_TICK_VALUE and SYMBOL_TRADE_TICK_SIZE via the broker API.
  4. 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.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Algorithmic Risk Management in MT5: ATR Volatility Position Sizing & MQL5 Architecture

Guetaquant - Sep 9

Dashboard Operasional Armada Rental Mobil dengan Python + FastAPI

Masbadar - Mar 12

Why Perfect Backtests Fail: Walk-Forward Optimization and Deflated Sharpe in Python

Guetaquant - Sep 9

cTrader cBot Risk Engine: High-Precision C# Dynamic Stop-Loss Architecture

Guetaquant - Sep 21

Pine Script v6 in 2026: math.sum, math.tanh, User Defined Types & Risk Management Engine

Guetaquant - Sep 9
chevron_left
853 Points17 Badges
Bogotá, Colombiaguetaquant.com
5Posts
2Comments
9Connections
Founder & Lead Quantitative Engineer at Gueta Quant (https://guetaquant.com).

I design open-source... Show more

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!