Skip to content
Friday, 24 July 2026 · LondonENع
Rayan Azhari.Sustainability · Energy · Carbon · Built EnvironmentOccasional detours into philosophy, religion or programming, wherever curiosity leads
Production Quant Trading

Never Use Float For Money: The Bug That Silently Mis-Sizes Live Trades

Using a floating-point number for money is a classic bug, but in a trading system it does not just round a penny wrong: it silently mis-sizes a live position. Here is why, and why a Money type is not the fix you think it is.

Rayan AzhariChartered Environmentalist, MISEP · 8 min read
Title card for the essay Never Use Float For Money, showing the classic 0.1 plus 0.2 floating-point result beside two bars, an intended position of 100 and a shipped position of 133, a third too large.

Open a Python prompt and type 0.1 + 0.2. You get 0.30000000000000004. Every programmer meets this once, shrugs, and files it under "floating-point is weird." In most software that shrug is fine. In a trading system it is how you ship a position a third larger than you intended, with no crash and no failing test to warn you.

The rule is old and boring: never store money in a floating-point number. What is worth saying, and what almost no version of this advice says, is why the usual fix does not go far enough once real money and more than one currency are involved. This piece is about the specific, expensive shape the bug takes in a system that actually places orders.

Why a float cannot hold money

Floating point is base-2. Money is base-10. The number 0.1 has no exact representation in IEEE-754, the same way 1/3 has no exact representation in decimal, so the moment you add a few of them the error stops hiding at the fifteenth digit and starts showing up in the tenth. That is not an academic curiosity. It compounds through position sizing, accumulates across fills, and eventually a quantity that should be a clean integer of shares is 99.999999998.

A broker will reject that quantity, or worse, silently round it in a direction you did not choose. Prices live on a tick grid and quantities live on a lot grid, and both grids are exact decimal steps. A float cannot sit on a decimal grid, so any money value you carry as a float is, by construction, slightly off the grid it has to land on. The fix every experienced engineer reaches for is the right one: use a base-10 decimal type (Python's Decimal), or your broker library's typed Price and Quantity objects, for anything that is a price, a quantity, a size, or an amount of money.

So far, this is the standard advice, and it is correct. Here is where trading makes it more interesting.

The bug is not a rounded penny. It is a mis-sized trade.

In a payments system the float bug costs you a cent and an angry reconciliation. In a trading system the same class of bug resizes your bet. And the most dangerous version has nothing to do with rounding at all. It is currency.

Chart

The same leg, two sizings

A leg quoted in one currency and sized from an account in another shipped about a third too large, because the FX conversion was a plain float multiply with the wrong rate. No crash, no failing test. Illustrative and sanitised.

Intended (correct FX)100 sharesShipped (untyped float FX)133 shares
Source: Building a Production Quant Trading System (Titan), sanitised, illustrative

A leg quoted in one currency, sized from an account denominated in another, went on about a third too large. The cause was mundane: the FX conversion was a plain, untyped float multiply, and it used the wrong rate for the leg. Nothing threw. No unit test failed, because the arithmetic was perfectly valid arithmetic. The number was simply the wrong number, and a wrong number that is the right type sails straight through to the broker and becomes a real, over-sized position.

That is the trading-specific lesson. A float bug in money does not announce itself with a stack trace. It announces itself, weeks later, as a drawdown you cannot explain, because the strategy you validated was quietly not the strategy you deployed. The backtest sized the leg correctly; the live path did not; and nothing in between was ever going to tell you.

Why a Money type is not the fix you think it is

The natural next thought is: "fine, I will wrap money in a Money type that carries its currency, and the compiler will catch this." It is the right instinct and, taken literally, it does not work. This is the part worth pinning up.

A stock Money(amount, currency) type, whether from a library or your standard library, carries a currency label and will happily let you multiply it by a bare number. It has to, because that is a legitimate operation: scaling a position, applying a fraction, and, fatally, converting between currencies is itself money times a scalar. FX conversion looks exactly like the safe operation, so the type system sees nothing wrong. You reach for Money expecting the compiler to save you, and against this specific bug you are exactly as unprotected as you were with a raw float.

The guardrail that actually works is narrower and stricter. You need a money type that refuses to combine two different currencies without an explicit, rate-checked convert() call. Adding US dollars to yen should not compile. Sizing a yen leg from a dollar budget should be unrepresentable unless the conversion is spelled out, with the rate passed in and checked. Once cross-currency arithmetic can only happen through one deliberate, auditable function, the war-story above becomes a type error at author time instead of a mystery in the P&L. The lesson is not "use a Money type." It is "use a money type whose illegal operations do not compile," which is a much smaller set of types than the ones that merely track a currency.

The discipline: a boundary, not a blanket ban

None of this means float is banned from your codebase. It means float has a border it may not cross.

Figure

Where float is fine, and where it is forbidden

The rule is a boundary, not a blanket ban. Float belongs in the statistics; it must never reach an order.

float is fine

the statistical layer

Returns, Sharpe, volatility, z-scores. These are dimensionless ratios, so base-2 rounding at the fifteenth digit changes nothing you could measure.

float is forbidden

anything that becomes an order

Prices, quantities, sizes and money sit on exact decimal grids. A float cannot land on a tick or lot grid, so 99.999999998 shares is either rejected or silently rounded the way you did not choose.

The boundary

convert once, at the edge

One function turns 'risk 1% of equity' into a typed quantity, snaps it to the grid with a stated rounding policy, and does it on the way out. That single conversion is the only place a float may touch a size.

The trap

a Money type is not enough

A stock Money carries a currency but still lets you multiply it by a bare rate, because FX conversion is money times a scalar. Only a type that refuses to combine two currencies without an explicit, rate-checked convert protects you.

Make the wrong thing impossible to write, not something to remember.

Source: Building a Production Quant Trading System (Titan)

Float is not just acceptable but preferable in the statistical layer: returns, Sharpe ratios, volatilities, z-scores. Those are dimensionless ratios, and base-2 rounding in the fifteenth decimal place is irrelevant to a number you are going to compare against a threshold. Forcing Decimal on your statistics buys you nothing and costs you speed and clarity.

The line is sharp: the moment a number becomes an order, it must be a typed money object. The clean place to enforce that is the single function at the edge of the strategy that turns an intention ("risk 1% of equity") into a concrete size. That one function is the only place a float is allowed to touch a quantity, and it converts on the way out, snapping the result to the instrument's grid.

And when it snaps to the grid, it must do so on purpose. 99.999999998 shares has to resolve to 99 or 100, and which is a policy you choose, not one you inherit. Decide explicitly whether you round half-up, half-even, or always toward the smaller, more conservative size, and round exactly once, at that boundary. Decimal defaults to banker's rounding while your broker may expect something else, and an unstated tie-break is one more place a wrong assumption waits for a fill to land on it.

The general rule this is an instance of

Step back and the money rule is a special case of a principle that pays off everywhere in a system that handles real capital: make the wrong thing impossible to write, rather than something to remember. A coding standard you have to keep in your head is one you will forget at 2am during the trade that matters. The standards that survive are the ones the tooling or the type system enforces for you.

Ranked by strength, you have three levers. Strongest: make the dangerous operation not exist, so a currency type with no un-checked cross-currency multiply simply cannot express the bug. Next: make it fail loudly, so a required argument with no default, or a check in CI, stops it before it runs. Weakest but still useful: make it conspicuous, so a raw float in the sizing path reads as an obviously wrong line in review. "Never use float for money" is memorable, but memory is the weakest lever of all. The durable version is a type that will not let you.

If you take one thing away: audit the single path in your system where an intention becomes an order, and make sure a raw float cannot survive the journey. Everything upstream of that boundary can stay float. Nothing downstream of it may.

This is one of four "non-negotiables" in the foundations of the system these essays come from. The full chapter, Project layout and non-negotiables, is free to read and sets the money rule alongside full typing, fixed random seeds, and one shared metrics module: four rules, each of which retires a whole class of recurring bug rather than a single instance.

This is an engineering essay, not investment advice, and it contains no tradable strategy. All figures are illustrative and sanitised.

Chart

The same leg, two sizings

A leg quoted in one currency and sized from an account in another shipped about a third too large, because the FX conversion was a plain float multiply with the wrong rate. No crash, no failing test. Illustrative and sanitised.

Intended (correct FX)100 sharesShipped (untyped float FX)133 shares
Source: Building a Production Quant Trading System (Titan), sanitised, illustrative

Figure

Where float is fine, and where it is forbidden

The rule is a boundary, not a blanket ban. Float belongs in the statistics; it must never reach an order.

float is fine

the statistical layer

Returns, Sharpe, volatility, z-scores. These are dimensionless ratios, so base-2 rounding at the fifteenth digit changes nothing you could measure.

float is forbidden

anything that becomes an order

Prices, quantities, sizes and money sit on exact decimal grids. A float cannot land on a tick or lot grid, so 99.999999998 shares is either rejected or silently rounded the way you did not choose.

The boundary

convert once, at the edge

One function turns 'risk 1% of equity' into a typed quantity, snaps it to the grid with a stated rounding policy, and does it on the way out. That single conversion is the only place a float may touch a size.

The trap

a Money type is not enough

A stock Money carries a currency but still lets you multiply it by a bare rate, because FX conversion is money times a scalar. Only a type that refuses to combine two currencies without an explicit, rate-checked convert protects you.

Make the wrong thing impossible to write, not something to remember.

Source: Building a Production Quant Trading System (Titan)

Further reading

Office energy, part 2 of 25

Related posts

Migrating from WordPress to Next.js: A Field Guide

A practical, end-to-end guide to moving a content site from WordPress to Next.js without losing your search rankings: the URL-preservation rule that governs everything, a content pipeline that survives the move, bilingual and RTL handling, the SEO and security work, and a cutover you can roll back.

· 20 min

I Stopped Paying for WordPress Hosting

Managed WordPress hosting is a tax you pay in money, in performance, and in security anxiety. Here is how I kept the WordPress editor I love, ran it locally for free, and served the public site as static HTML on the edge: a zero-dependency Node crawler I have open-sourced.

· 7 min

Essays in your inbox

New writing on Syria, sustainability and finance, a few times a month.

Unsubscribe anytime. Read by 4,200+ professionals.