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:
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¶
- Validate external data - Every field from a WebSocket deserves validation
- Bound memory - Attackers can craft messages to exhaust memory
- Don't block - Async channels with bounded capacity need non-blocking sends
- Use sorted containers - BTreeMap made bid/ask sorting trivial
The Kalshi WebSocket implementation is now feature-complete with proper delta handling.