Skip to content

Float safety: Kalshi order book NO-side price inversion Math.round((1 - parseFloat) * 10000) / 10000 #229

@realfishsam

Description

@realfishsam

Risk Level

HIGH

Location

core/src/exchanges/kalshi/normalizer.ts:167,176 and core/src/exchanges/kalshi/websocket.ts:261,328

Code

// normalizer.ts lines 166-168 (REST order book)
asks = (data.no_dollars || []).map((level) => ({
    price: Math.round((1 - parseFloat(level[0])) * 10000) / 10000,
    size: parseFloat(level[1]),
}));

// websocket.ts lines 260-262 (WS snapshot)
asks = (data.no_dollars_fp || []).map((level: [string, string]) => ({
    price: Math.round((1 - parseFloat(level[0])) * 10000) / 10000,
    size: parseFloat(level[1]),
}));

// websocket.ts line 328 (WS delta)
price = Math.round((1 - rawPrice) * 10000) / 10000;

Problem

Kalshi YES/NO markets require inverting NO prices to get the YES equivalent (yesPrice = 1 - noPrice). The subtraction 1 - parseFloat(...) is pure float arithmetic. Math.round(x * 10000) / 10000 is a band-aid that coerces the result to 4 decimal places but still via float multiplication and division.

These prices are stored in the live order book and used for quoting. A wrong price in the book means wrong spread, wrong best bid/ask, and wrong execution simulation.

Example Failure

noPrice string = "0.4450"
parseFloat("0.4450") = 0.445
1 - 0.445 = 0.5549999999999999  (IEEE 754: not 0.555)
* 10000 = 5549.999999999999
Math.round(...) = 5550
/ 10000 = 0.555  ← happens to be correct here

noPrice string = "0.3330"
1 - 0.333 = 0.6669999999999998
* 10000 = 6669.999999999998
Math.round(...) = 6670
/ 10000 = 0.667  ← correct by luck

noPrice string = "0.1115"
1 - 0.1115 = 0.8885000000000001
* 10000 = 8885.000000000001
Math.round(...) = 8885
/ 10000 = 0.8885  ← correct, but purely by luck; at 4dp the rounding is fragile

With 4 decimal places the band-aid usually works but is not guaranteed for all possible Kalshi prices.

Suggested Fix

import Decimal from 'decimal.js';
price: new Decimal(1).minus(level[0]).toDecimalPlaces(4).toNumber()

Found by automated float safety audit

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions