Skip to content

Float safety: Gemini Titan normalizer.ts roundPrice Math.round band-aid with self-documenting comment #674

@realfishsam

Description

@realfishsam

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

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