Summary
WebSocket payload models silently regress to plain str for count/size/volume/timestamp fields that REST models type as FixedPointCount / DollarDecimal / datetime. The same logical quantity (e.g. count, volume, created_ts) round-trips as Decimal/datetime on REST and as raw string on WS.
Consumers driving limits, hedging, P&L, or cross-stream correlation off WS must Decimal(...) / datetime.fromisoformat(...) every field by hand. Any oversight is a silent type error:
str + int → TypeError
str * 2 → doubled string (not doubled number)
- mixing REST
Decimal positions with WS str fills → comparison silently false
Decimal('5.00') != '5.00' quietly
Even within kalshi/ws/models/, types are inconsistent across closely-related payloads.
Affected fields
Count/size/volume (should be FixedPointCount per CHANGELOG v2.0 retyping)
kalshi/ws/models/ticker.py:34-50 — volume, open_interest, yes_bid_size, yes_ask_size, last_trade_size
kalshi/ws/models/trade.py:21-26 — count
kalshi/ws/models/fill.py:25-32 — count, post_position
kalshi/ws/models/user_orders.py:33-43 — fill_count, remaining_count, initial_count
kalshi/ws/models/market_positions.py:19-37 — position, volume
kalshi/ws/models/orderbook_delta.py:18-26 — OrderbookSnapshotPayload.yes / .no is list[tuple[str, str]]
Note the internal inconsistency: OrderbookDeltaPayload (same module) correctly uses DollarDecimal/FixedPointCount. So the same logical pair (price, qty) is typed differently on snapshot vs delta.
RFQ/Quote contracts typed inconsistently
kalshi/ws/models/communications.py:31-34 — RfqCreatedPayload.contracts: str | None
kalshi/ws/models/communications.py:64-68 — RfqDeletedPayload.contracts: FixedPointCount | None ← already correct
kalshi/ws/models/communications.py:117-120 — QuoteAcceptedPayload.contracts_accepted: str | None
Timestamps (should be datetime / pydantic.AwareDatetime per REST convention)
kalshi/ws/models/user_orders.py:57-59 — created_time, last_update_time, expiration_time (all str)
kalshi/ws/models/communications.py:30, 52, 79, 114, 158, 163 — created_ts, deleted_ts, executed_ts, etc. (with an inline TODO(v0.15.1) comment acknowledging the drift)
REST equivalent: kalshi/models/orders.py:88-89 created_time: datetime | None.
Recommended fix
Replace str with FixedPointCount / DollarDecimal on every _fp / _dollars-aliased field, and with datetime (or pydantic.AwareDatetime to force tz-awareness) on every ISO-8601 timestamp field. Pydantic handles both coercions automatically via the existing BeforeValidator and built-in datetime parser.
For OrderbookSnapshotPayload.yes/.no, change list[tuple[str, str]] to list[tuple[DollarDecimal, FixedPointCount]] (Pydantic coerces per element). Drop the manual Decimal(...) coercion in kalshi/ws/orderbook.py:79-80.
Update fixtures in tests/ws/test_models.py to match — the wire format does not change.
Add a parametrized contract test that asserts type symmetry across REST and WS for shared logical fields (count, volume, created_time).
Severity & category
high / consistency, ws
Summary
WebSocket payload models silently regress to plain
strfor count/size/volume/timestamp fields that REST models type asFixedPointCount/DollarDecimal/datetime. The same logical quantity (e.g.count,volume,created_ts) round-trips as Decimal/datetime on REST and as raw string on WS.Consumers driving limits, hedging, P&L, or cross-stream correlation off WS must
Decimal(...)/datetime.fromisoformat(...)every field by hand. Any oversight is a silent type error:str + int→ TypeErrorstr * 2→ doubled string (not doubled number)Decimalpositions with WSstrfills → comparison silently falseDecimal('5.00') != '5.00'quietlyEven within
kalshi/ws/models/, types are inconsistent across closely-related payloads.Affected fields
Count/size/volume (should be
FixedPointCountper CHANGELOG v2.0 retyping)kalshi/ws/models/ticker.py:34-50—volume,open_interest,yes_bid_size,yes_ask_size,last_trade_sizekalshi/ws/models/trade.py:21-26—countkalshi/ws/models/fill.py:25-32—count,post_positionkalshi/ws/models/user_orders.py:33-43—fill_count,remaining_count,initial_countkalshi/ws/models/market_positions.py:19-37—position,volumekalshi/ws/models/orderbook_delta.py:18-26—OrderbookSnapshotPayload.yes/.noislist[tuple[str, str]]Note the internal inconsistency:
OrderbookDeltaPayload(same module) correctly usesDollarDecimal/FixedPointCount. So the same logical pair(price, qty)is typed differently on snapshot vs delta.RFQ/Quote
contractstyped inconsistentlykalshi/ws/models/communications.py:31-34—RfqCreatedPayload.contracts: str | Nonekalshi/ws/models/communications.py:64-68—RfqDeletedPayload.contracts: FixedPointCount | None← already correctkalshi/ws/models/communications.py:117-120—QuoteAcceptedPayload.contracts_accepted: str | NoneTimestamps (should be
datetime/pydantic.AwareDatetimeper REST convention)kalshi/ws/models/user_orders.py:57-59—created_time,last_update_time,expiration_time(allstr)kalshi/ws/models/communications.py:30, 52, 79, 114, 158, 163—created_ts,deleted_ts,executed_ts, etc. (with an inlineTODO(v0.15.1)comment acknowledging the drift)REST equivalent:
kalshi/models/orders.py:88-89created_time: datetime | None.Recommended fix
Replace
strwithFixedPointCount/DollarDecimalon every_fp/_dollars-aliased field, and withdatetime(orpydantic.AwareDatetimeto force tz-awareness) on every ISO-8601 timestamp field. Pydantic handles both coercions automatically via the existingBeforeValidatorand built-in datetime parser.For
OrderbookSnapshotPayload.yes/.no, changelist[tuple[str, str]]tolist[tuple[DollarDecimal, FixedPointCount]](Pydantic coerces per element). Drop the manualDecimal(...)coercion inkalshi/ws/orderbook.py:79-80.Update fixtures in
tests/ws/test_models.pyto match — the wire format does not change.Add a parametrized contract test that asserts type symmetry across REST and WS for shared logical fields (
count,volume,created_time).Severity & category
high / consistency, ws