Summary
A handful of money-adjacent fields are typed as bare Decimal or float, skipping the _coerce_decimal invariant set by #225:
Market.floor_strike: Decimal | None, cap_strike: Decimal | None
Event.fee_multiplier_override: Decimal | None
MarketLifecyclePayload.floor_strike: Decimal | None
Series.fee_multiplier: float, SeriesFeeChange.fee_multiplier: float
Bare Decimal skips _coerce_decimal — Pydantic will accept a JSON-number 1.23 by routing through float first, exactly the precision-loss path the SDK paid an audit to eliminate. fee_multiplier: float is worse: the value is committed to a binary float on parse (0.65 -> 0.6500000000000000222...) which then propagates into any cost computation. None of these fields reject bool (the #225 invariant). Strikes and multipliers are load-bearing in payout math.
Location
kalshi/models/markets.py:117-118
kalshi/models/events.py:39
kalshi/ws/models/market_lifecycle.py:33
kalshi/models/series.py:30
kalshi/models/series.py:48
Evidence
# markets.py Market
floor_strike: Decimal | None = None
cap_strike: Decimal | None = None
# events.py Event
fee_multiplier_override: Decimal | None = None
# market_lifecycle.py MarketLifecyclePayload
floor_strike: Decimal | None = None
# series.py Series / SeriesFeeChange
fee_multiplier: float
Recommended fix
Switch floor_strike/cap_strike to DollarDecimal (they are dollar values per spec). Switch fee_multiplier/fee_multiplier_override to Decimal with BeforeValidator(_coerce_decimal) (or introduce a MultiplierDecimal alias) so the parse path matches every other numeric SDK field.
Severity & category
medium / correctness
Summary
A handful of money-adjacent fields are typed as bare
Decimalorfloat, skipping the_coerce_decimalinvariant set by #225:Market.floor_strike: Decimal | None,cap_strike: Decimal | NoneEvent.fee_multiplier_override: Decimal | NoneMarketLifecyclePayload.floor_strike: Decimal | NoneSeries.fee_multiplier: float,SeriesFeeChange.fee_multiplier: floatBare
Decimalskips_coerce_decimal— Pydantic will accept a JSON-number1.23by routing throughfloatfirst, exactly the precision-loss path the SDK paid an audit to eliminate.fee_multiplier: floatis worse: the value is committed to a binary float on parse (0.65->0.6500000000000000222...) which then propagates into any cost computation. None of these fields reject bool (the #225 invariant). Strikes and multipliers are load-bearing in payout math.Location
kalshi/models/markets.py:117-118kalshi/models/events.py:39kalshi/ws/models/market_lifecycle.py:33kalshi/models/series.py:30kalshi/models/series.py:48Evidence
Recommended fix
Switch
floor_strike/cap_striketoDollarDecimal(they are dollar values per spec). Switchfee_multiplier/fee_multiplier_overridetoDecimalwithBeforeValidator(_coerce_decimal)(or introduce aMultiplierDecimalalias) so the parse path matches every other numeric SDK field.Severity & category
medium / correctness