Precision is over the maximum defined for this asset: float math in a trading bot
A grid trading bot written in TypeScript sent quantities like 0.30000000000000004 and got error -1111. Binary floating point cannot represent most decimal fractions, and serialising numbers to strings exposed it. A decimal library end to end and string serialisation fixed it.
We wrote a grid trading bot in TypeScript for a few spot pairs. It places buy and sell orders at fixed price steps and adjusts quantities as orders fill. Most orders were accepted. Some were rejected:
POST /api/v3/order symbol=ETHUSDT side=SELL type=LIMIT quantity=0.30000000000000004 price=2481.37
{"code":-1111,"msg":"Precision is over the maximum defined for this asset."}
and a few others had prices like 2479.3900000000003.
Reproducing it
const filled = 0.1;
const remaining = 0.2;
console.log(filled + remaining); // 0.30000000000000004
console.log(2481.37 - 1.98); // 2479.3900000000003
The bot added and subtracted quantities as fills arrived and computed grid prices by repeated addition of a step.
What we ruled out
- Wrong step or tick configuration. The intended values were on the grid; the computed values were off by tiny amounts.
- Exchange-side changes. The symbol's filters had not changed.
Root cause
JavaScript numbers are IEEE 754 double-precision binary floats. Decimal fractions like 0.1 and 0.2 have no exact binary representation, so arithmetic produces values like 0.30000000000000004. When serialised with String() or JSON, the full representation is sent. The exchange enforces maximum decimal places per asset and rejects values with more. Repeated addition also accumulated error, so grid prices drifted off the tick grid over time.
What is happening under the hood
A double has 53 bits of binary mantissa. Numbers like 0.5 or 0.25 are exact; 0.1 is stored as the nearest binary fraction, which is slightly above 0.1. Most of the time, printing rounds this away (0.1 prints as 0.1), but after arithmetic the nearest representable result may print with many digits.
For money and quantities with fixed decimal rules, you want decimal arithmetic: numbers stored as an integer coefficient and a decimal exponent, where 0.1 + 0.2 is exactly 0.3. Alternatives are integer arithmetic in the smallest unit (for example, quantity in units of the step size) or a decimal library.
Environment
| Component | Version / value |
|---|---|
| Runtime | Node.js 22 |
| Language | TypeScript 5.6 |
| HTTP client | undici fetch |
| Exchange | Binance Spot REST |
| Arithmetic | JavaScript number throughout |
Minimal reproduction
let price = 2400;
for (let i = 0; i < 50; i++) price += 1.98;
console.log(price); // not exactly 2499.00, e.g. 2499.0000000000027
The fix
1. Decimal type end to end. All prices, quantities and balances use decimal.js, created from strings returned by the exchange, never from JavaScript numbers:
import Decimal from "decimal.js";
Decimal.set({ precision: 40, rounding: Decimal.ROUND_DOWN });
function gridPrices(base: string, step: string, levels: number): Decimal[] {
const b = new Decimal(base), s = new Decimal(step);
return Array.from({ length: levels }, (_, i) => b.plus(s.times(i))); // computed, not accumulated
}
function toStep(value: Decimal, step: string): string {
const s = new Decimal(step);
return value.div(s).floor().times(s).toFixed(s.decimalPlaces());
}
2. Strings on the wire. Orders are serialised with toFixed(decimalPlaces) derived from the symbol's step and tick sizes, so the request contains exactly the digits the exchange expects.
3. Parse responses as strings. The exchange returns quantities as strings; we stopped converting them to number in the HTTP layer. JSON parsing of numeric fields (where they appear as numbers) uses a reviver that keeps them as strings for designated fields.
4. Grid prices computed from the base, base + step * i, instead of repeated addition, so error cannot accumulate.
Alternatives and trade-offs
- Integers in minimal units. Represent quantity as an integer number of step units (
BigIntin JavaScript). Fast and exact, more conversion code. - Round floats before sending.
Number(x.toFixed(n))hides the symptom; values used in further math still drift, and comparisons against balances can be wrong.
How to test the fix
Property-based tests with fast-check generate random fills and grid configurations and assert that every serialised quantity and price has at most the allowed decimals and lies on the step and tick grid. A replay of a week of real fills produced identical position quantities to the exchange's account balances, to the last digit.
Confirming in production
- Zero -1111 rejections since the change.
- Position quantities in the bot match exchange balances exactly at every reconciliation, where before they drifted by tiny amounts that occasionally caused "insufficient balance" errors on the final sell.
Regression guard
- A lint rule forbids
numbertypes for fields namedprice,qty,quantity,amountorbalancein the trading modules. - The property tests in CI.
- Reconciliation alerts on any difference between bot and exchange balances.
What we would do differently
Choose the numeric type on day one. Money and quantities were number because the first prototype was a weekend script, and nobody revisited the choice when it became a service. A short design note at the start, "all amounts are decimal strings on the wire and Decimal in memory", would have cost an hour and avoided weeks of small, confusing mismatches. We now include that note in the template for every new trading service, together with a ready-made helper module for formatting quantities and prices per symbol.
Security implications
Precision bugs are integrity bugs in money handling. Silent drift in position quantities can lead to orders that exceed intended exposure or fail at critical moments, like an exit. Using exact arithmetic and reconciling against the exchange are core controls. The bot's API key cannot withdraw funds and is restricted by IP.
Prevention checklist
- Never use binary floating point for prices, quantities or balances.
- Construct decimals from strings, and send strings.
- Compute values from a base, not by repeated addition.
- Format to the exact decimals allowed by the symbol's filters.
- Reconcile positions against the exchange regularly.
References
- Binance Spot API error codes (-1111) — https://developers.binance.com/docs/binance-spot-api-docs/errors
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
- decimal.js — https://mikemcl.github.io/decimal.js/
- MDN, Number type and precision — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
Add a label
Embed this post
<iframe src="https://hibug.me/trading-bot-float-precision-over-maximum" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.
Related Stories
Order updates stopped after an hour: user data stream listenKey expired without keepalive
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.