Skip to content

Polish bundle: WS small items (Full Jitter reconnect, side Literal, populate_by_name, AwareDatetime, sentinel idempotence) #221

Description

@TexasCoding

Summary

Bundle of small WebSocket polish items.

1. Use AWS Full Jitter for WS reconnect (match REST)

Current WS reconnect backoff: base_delay * 2**attempt + uniform(0, 0.5), capped at retry_max_delay. At attempt ≥ 6 the delay is at the cap (30s) and the jitter is a flat 0–0.5s — all clients that disconnect simultaneously (global Kalshi WS outage) reconnect within a 0.5s window. Thundering-herd risk.

REST already uses AWS Full Jitter (random.uniform(0, min(retry_max_delay, retry_base_delay * 2**attempt))) per CHANGELOG. WS reconnect was not migrated.

Fix: Switch WS reconnect to the same Full Jitter formula:

delay = random.uniform(0, min(retry_max_delay, retry_base_delay * (2 ** attempt)))

2. OrderbookDeltaPayload.side is bare str

apply_delta does levels = state.yes if side == 'yes' else state.no. Any non-'yes' value silently routes to the no-side book — 'no ' (trailing space), uppercase, or any future server value silently corrupts the no-side.

TradePayload/FillPayload/UserOrdersPayload already use SideLiteral/BookSideLiteral. OrderbookDeltaPayload wasn't tightened.

Fix: type as Literal["yes", "no"] (or shared BookSideLiteral). Pydantic rejects malformed sides at parse; dispatcher's existing except ValidationError drops the bad frame.

3. WS payload models lack populate_by_name=True

Every REST response/request model with validation_alias also sets model_config = {'extra': 'allow', 'populate_by_name': True} so callers can construct instances with the SDK field name. All eight WS payload modules with validation_alias forget populate_by_name=True, so a test fixture or replay tool must use the wire name.

Fix: Add 'populate_by_name': True to model_config on every WS payload class that uses validation_alias. Mechanical change; no runtime effect on parsing the wire format.

4. Datetime fields accept naive (tz-less) values silently

Models declare fields as bare datetime, which Pydantic v2 accepts both tz-aware and tz-naive values. Kalshi wire format is always RFC3339 with Z (UTC) so live parsing is fine, but users constructing models for tests can pass datetime.now() (naive) and the model silently accepts it. Trading SDKs and tz arithmetic bugs are a known landmine.

Fix: Replace datetime with pydantic.AwareDatetime on all response-model datetime fields. Pydantic rejects naive datetimes at construction. Note: depends on / pairs with H8 (WS timestamp fields). Add a "datetime semantics" note to docs/concepts.md.

5. put_sentinel docstring claim wrong at boundary

The _broadcast_sentinels docstring asserts: "put_sentinel is idempotent — calling twice puts two sentinels in the queue, the iterator reads the first and raises StopAsyncIteration without ever touching the second." With deque(maxlen=maxsize+1), when the buffer is at maxsize real items + 1 sentinel, a second put_sentinel evicts the leftmost element silently.

Fix: Make put_sentinel a no-op when _closed is already True (the contract is purely "wake the consumer"). Removes any chance of evicting an unread real item.

Severity & category

Bundle of low items, mostly ws + polish.

Metadata

Metadata

Assignees

No one assigned

    Labels

    polishCode-quality and DX improvements; non-functionalwsWebSocket-related

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions