Skip to content

Kalshi WebSocket Delta Application

Implementing efficient orderbook state management for real-time market data using BTreeMap and incremental delta updates.

The Problem

Exchange WebSocket APIs typically send orderbook data in two formats:

Message Type Contents When Sent
Snapshot Complete orderbook state On subscription
Delta Incremental changes Per update

The naive approach processes only snapshots, but this wastes bandwidth and introduces latency. The orderbook becomes stale between snapshots. For arbitrage detection, stale data means missed opportunities or erroneous trades.

Decision: Local State Machine

We implemented a LocalOrderbook struct that maintains state between deltas:

struct LocalOrderbook {
    market_ticker: String,
    yes_levels: BTreeMap<i64, i64>,  // price -> quantity
    no_levels: BTreeMap<i64, i64>,
}

Why BTreeMap?

Prediction market orderbooks require sorted price levels: - Bids: Sorted descending (best bid first) - Asks: Sorted ascending (best ask first)

BTreeMap provides O(log n) insert/update/delete with automatic sorting. When converting to our OrderBook type, we simply iterate in the appropriate direction.

Delta Application Logic

The delta protocol is simple: - Positive delta: Add contracts at price level - Negative delta: Remove contracts - Zero total: Remove the level entirely

fn apply_delta(&mut self, delta: &OrderbookDelta) {
    let levels = match delta.side.as_str() {
        "yes" => &mut self.yes_levels,
        "no" => &mut self.no_levels,
        _ => return,
    };

    let current = levels.get(&delta.price).copied().unwrap_or(0);
    let new_quantity = current.saturating_add(delta.delta);

    if new_quantity <= 0 {
        levels.remove(&delta.price);
    } else {
        levels.insert(delta.price, new_quantity);
    }
}

Note saturating_add() prevents integer overflow from malicious deltas.

Security Hardening

Market data comes from external sources. We added several defensive measures:

Price Validation

Kalshi prices are in cents (1-99). Invalid prices are rejected:

const MIN_PRICE: i64 = 1;
const MAX_PRICE: i64 = 99;

fn is_valid_price(price: i64) -> bool {
    price >= MIN_PRICE && price <= MAX_PRICE
}

Memory Bounds

A malicious feed could send unlimited price levels. We cap at 200:

const MAX_LEVELS: usize = 200;

// In apply_delta:
if !levels.contains_key(&delta.price) && levels.len() >= MAX_LEVELS {
    return; // Reject new levels when at capacity
}

Non-Blocking Sends

The WebSocket message loop must not block. We use try_send() instead of send().await:

let _ = self.arbiter_tx.try_send(ArbiterMsg::MarketUpdate(orderbook));

If the ArbiterActor is slow, updates are dropped rather than blocking the WebSocket connection. This prevents a slow consumer from causing WebSocket disconnects.

Kalshi Price Conversion

Kalshi uses a YES/NO binary market model. The conversion to bid/ask is:

Kalshi Side OrderBook Side Price Conversion
YES Bid price / 100
NO Ask (100 - price) / 100

The NO price represents how much you'd pay to bet against YES. So NO@56 means you can buy YES at (100-56)/100 = 0.44.

Test Coverage

We follow TDD as required by CLAUDE.md. The test suite covers:

Test Purpose
test_delta_application_add_level New price levels
test_delta_application_update_level Quantity changes
test_delta_application_remove_level Level removal
test_delta_sequence_produces_correct_orderbook End-to-end state
test_invalid_price_filtered_in_snapshot Security validation
test_price_boundary_values Edge cases (1 and 99)

16 total tests provide confidence in the implementation.

Integration

The message loop now handles both message types:

match self.parse_message(&txt) {
    Ok(ParsedMessage::Snapshot(snapshot)) => {
        local.apply_snapshot(&snapshot);
        let orderbook = local.to_orderbook();
        let _ = self.arbiter_tx.try_send(...);
    }
    Ok(ParsedMessage::Delta(delta)) => {
        if let Some(ref mut local) = self.local_orderbook {
            local.apply_delta(&delta);
            let orderbook = local.to_orderbook();
            let _ = self.arbiter_tx.try_send(...);
        }
    }
    Ok(ParsedMessage::Control) => {}
    Err(e) => println!("[KalshiMonitor] Parse error: {}", e),
}

Deltas are only applied when local state exists (after first snapshot).

Lessons Learned

  1. Validate external data - Every field from a WebSocket deserves validation
  2. Bound memory - Attackers can craft messages to exhaust memory
  3. Don't block - Async channels with bounded capacity need non-blocking sends
  4. Use sorted containers - BTreeMap made bid/ask sorting trivial

The Kalshi WebSocket implementation is now feature-complete with proper delta handling.