Risk Level
MEDIUM
Location
core/src/exchanges/gemini-titan/normalizer.ts:81-86
Code
/**
* Round to 2 decimal places to avoid floating point noise.
*/
function roundPrice(n: number): number {
return Math.round(n * 100) / 100;
}
Problem
This function is an explicitly documented band-aid — its JSDoc says "to avoid floating point noise." The same Math.round(x * 100) / 100 pattern that affects Kalshi's NO-side price inversion (#229) is applied here to Gemini Titan order book prices. The function exists specifically because float prices arriving from the Gemini Titan API or computed locally carry rounding noise that breaks downstream comparisons.
n * 100 is IEEE 754 float multiplication. For any price n with more than 2 significant decimal digits, this multiplication can produce a dirty intermediate. Math.round on the dirty intermediate rounds to the wrong cent for inputs near .xx5 boundaries. The result is stored as the book level price and passed to the WebSocket applyDelta comparison (which already has its own tolerance issue, #275).
The band-aid masks the root cause (float prices from parseFloat at normalizer entry) and fails for edge-case inputs.
Example Failure
// parseFloat("0.575") * 100 = 57.49999999999999 → Math.round → 57 → / 100 = 0.57
// (same as the Kalshi * 100 example in issue #202)
// A price computed by subtraction — e.g. 1 - 0.425 in a complement calculation:
1 - 0.425 = 0.5750000000000001 (IEEE 754)
0.5750000000000001 * 100 = 57.50000000000001
Math.round(57.50...) = 58 // correct here
// But:
1 - 0.4250000000000001 = 0.5749999999999999
0.5749... * 100 = 57.499...
Math.round(57.499...) = 57 // wrong — order book shows 0.57 instead of 0.575
Note: Gemini Titan uses 2dp precision (cents), so the effective tick size is 0.01. A 1-cent error in a book level corrupts the spread and best bid/ask reported to the router.
Suggested Fix
Remove the need for roundPrice by parsing prices correctly at the API boundary using decimal.js. The function should not exist if prices enter as strings and are parsed with new Decimal(str):
// In normalizer, replace parseFloat(level.price) with:
price: new Decimal(level.price).toDecimalPlaces(2).toNumber(),
Found by automated float safety audit
Risk Level
MEDIUM
Location
core/src/exchanges/gemini-titan/normalizer.ts:81-86Code
Problem
This function is an explicitly documented band-aid — its JSDoc says "to avoid floating point noise." The same
Math.round(x * 100) / 100pattern that affects Kalshi's NO-side price inversion (#229) is applied here to Gemini Titan order book prices. The function exists specifically because float prices arriving from the Gemini Titan API or computed locally carry rounding noise that breaks downstream comparisons.n * 100is IEEE 754 float multiplication. For any pricenwith more than 2 significant decimal digits, this multiplication can produce a dirty intermediate.Math.roundon the dirty intermediate rounds to the wrong cent for inputs near.xx5boundaries. The result is stored as the book level price and passed to the WebSocketapplyDeltacomparison (which already has its own tolerance issue, #275).The band-aid masks the root cause (float prices from
parseFloatat normalizer entry) and fails for edge-case inputs.Example Failure
Note: Gemini Titan uses 2dp precision (cents), so the effective tick size is 0.01. A 1-cent error in a book level corrupts the spread and best bid/ask reported to the router.
Suggested Fix
Remove the need for
roundPriceby parsing prices correctly at the API boundary usingdecimal.js. The function should not exist if prices enter as strings and are parsed withnew Decimal(str):Found by automated float safety audit