Skip to content

WS: orderbook permanently desyncs after a sequence gap (no resnapshot) #189

Description

@TexasCoding

Summary

Critical: orderbook permanently desyncs after a single sequence gap.

_handle_seq_gap (in kalshi/ws/client.py) clears the affected local books and resets the sequence tracker, but never requests a fresh snapshot, never re-subscribes, and never invalidates the subscription. Kalshi only sends an orderbook_snapshot on subscribe, so once a gap fires:

  1. Local books are wiped via OrderbookManager.remove(ticker).
  2. Every subsequent orderbook_delta arriving for that ticker hits OrderbookManager.apply_delta, finds no _BookState, and silently returns None (DEBUG-level warning only).
  3. The consumer iterator keeps receiving delta envelopes — but the local book stays permanently empty.

This is silent data divergence on the most safety-critical channel of the SDK. Any production orderbook strategy that experiences a single dropped frame will operate against an empty book forever after.

Compounding issue: all-markets orderbook subscriptions (subscribe_orderbook_delta() with no tickers=) have sub.params['market_tickers'] empty, so _handle_seq_gap iterates an empty list and removes no books — every locally-maintained book on that sid then silently diverges with no remediation.

Location

  • kalshi/ws/client.py:380-403_handle_seq_gap
  • kalshi/ws/orderbook.py:91-105apply_delta silently returns None for missing book

Evidence

# kalshi/ws/client.py:380-403
async def _handle_seq_gap(self, gap: SequenceGap) -> None:
    """Handle a sequence gap by logging and triggering resync."""
    logger.warning("Sequence gap on sid %d: ... Triggering resync.", ...)
    if self._sub_mgr:
        sub = self._sub_mgr.get_subscription_by_sid(gap.sid)
        if sub and sub.channel == "orderbook_delta":
            tickers = sub.params.get("market_tickers", [])  # empty if all-markets sub
            if self._orderbook_mgr:
                for ticker in tickers:
                    self._orderbook_mgr.remove(ticker)
            if self._seq_tracker:
                self._seq_tracker.reset(gap.sid)
    # ← returns; never resubscribes, never asks server for snapshot
# kalshi/ws/orderbook.py:91-105
def apply_delta(self, msg: OrderbookDeltaMessage) -> Orderbook | None:
    ticker = msg.msg.market_ticker
    state = self._books.get(ticker)
    if state is None:
        logger.warning("Delta for unknown ticker %s (no snapshot yet)", ticker)
        return None  # silently swallow → consumer's local book is forever empty

Recommended fix

Drive real resync from _handle_seq_gap:

  1. For each affected subscription (orderbook_delta and any other sequenced channel — order_group_updates has the same problem; see separate issue), unsubscribe and re-subscribe with send_initial_snapshot=True.
  2. For all-markets orderbook subscriptions, track which tickers a sid has produced snapshots for in OrderbookManager state, then clear all of them before resubscribe.
  3. Until real resync ships, the gap MUST surface to the consumer iterator (typed envelope like OrderbookResyncRequired or a KalshiSequenceGapError raised through __anext__) so safety-critical strategy code can halt rather than silently consume a permanently-empty book.

Add an integration test that injects a forced gap (manipulate SequenceTracker._last_seq) on both a per-ticker and an all-markets subscribe_orderbook_delta and asserts the book repopulates within N frames.

Severity & category

critical / correctness, ws

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity-related concernwsWebSocket-related

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions