Advertisement IC Markets

floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

Share, develop, and backtest custom MQL4/MQL5 Expert Advisors, Python data-scraping scripts, trading bots, and automated market alert systems.
Post Reply
Fairman
Posts: 6757
Joined: Tue Jul 21, 2026 7:11 am
Location: Abuja

floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

Post by Fairman »

floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

My lot size function rounded down to the broker's lot step, and for a $29 risk with a 10 pip stop it gave 0.28 instead of 0.29. Here is the bug in Python, which is the same in most languages.

Code: Select all

import math
risk, stop_pips, pip_value, step = 29.0, 10, 10.0, 0.01
raw = risk / (stop_pips * pip_value)    # 0.29
print(raw / step)                       # 28.999999999999996
print(math.floor(raw / step) * step)    # 0.28
0.29 can't be represented exactly in binary, so dividing by 0.01 lands a hair under 29, and floor takes it to 28. The size is then 0.28 lots, so the risk is $28 instead of $29, about 3.4% under plan. I counted the sizes from 0.01 to 1.99 in steps of 0.01 and 14 of the 199 hit this, including 0.29, 0.47, 0.57 and 1.13.

Two fixes that work. One is to add a small tolerance before flooring, with math.floor(raw / step + 1e-9) * step. The other is to use Decimal for money and sizes.

Code: Select all

from decimal import Decimal, ROUND_DOWN
lots = Decimal(str(risk)) / (Decimal(stop_pips) * Decimal(str(pip_value)))
lots = lots.quantize(Decimal("0.01"), rounding=ROUND_DOWN)   # 0.29
I now have a unit test that loops over every size from 0.01 to 5.00 and checks that sizing back from the risk it implies returns the same lots.
It’s Fairman :geek:
Recommended broker for automated trading & scalping IC Markets
Fairman
Posts: 6757
Joined: Tue Jul 21, 2026 7:11 am
Location: Abuja

Re: floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

Post by Fairman »

The same issue shows up in MQL as well, since it also uses doubles. A common pattern is MathFloor(lots / lotStep) * lotStep, and it fails on the same values. The usual fix is to add a tiny epsilon like 1e-8 inside the floor, or to round the ratio first with MathRound(lots / lotStep * 1e6) / 1e6 and floor that. Test with 0.29 and a 0.01 step, because those are the numbers that trip it. If your EA sizes 0.28 where you expected 0.29, you now know where to look, and the cost of the bug is a consistent under-sizing of about 3% on those lots.
It’s Fairman :geek:
Fairman
Posts: 6757
Joined: Tue Jul 21, 2026 7:11 am
Location: Abuja

Re: floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

Post by Fairman »

A test that finds this kind of problem automatically is to loop through every lot size and push it through the pipeline backwards. For lots in 0.01, 0.02 and up to 5.00, compute the dollar risk the size implies for a given stop, feed that risk into the sizing function, and assert that you get the same lots back. That's 500 checks that take a few milliseconds, and it catches rounding errors in both directions. With the naive version I found 14 failures among the first 199 sizes, up to 1.99 lots. After the epsilon fix the count was zero.
It’s Fairman :geek:
Fairman
Posts: 6757
Joined: Tue Jul 21, 2026 7:11 am
Location: Abuja

Re: floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

Post by Fairman »

Rounding down is a design choice, and it's a decision to make on purpose. Rounding down never exceeds the risk limit, but it can leave up to one step unused. With a 0.01 step and a 10 pip stop, one step is $1 of risk. On a $10,000 account risking $100 that's 1% of the allowance. On a $200 account risking $2, the exact size is 0.02 lots and a single step is half the risk. Know your step before you pick a risk percentage. Some brokers also set a minimum lot above 0.01 on certain symbols, such as indices or metals, and then the floor matters as much as the step, because the smallest allowed trade may already risk more than your plan.
It’s Fairman :geek:
Fairman
Posts: 6757
Joined: Tue Jul 21, 2026 7:11 am
Location: Abuja

Re: floor(risk / stop / step) gave 0.28 lots instead of 0.29: the float trap in sizing

Post by Fairman »

The same trap affects comparisons. Writing if lots == 0.29 or if risk_used <= max_risk fails when a value is 0.28999999999999998 and the limit is 0.29. Use a tolerance for comparisons, such as abs(a-b) < 1e-9, or do the check in integer units, with pips as integers and lots as hundredths. Keeping lots as an integer count of steps, so 29 instead of 0.29, takes the problem away entirely and converts to a float only when sending the order. It's a small change in the code and it removes a family of bugs.
It’s Fairman :geek:
Post Reply