feat(fix): market-data session + order-book reconstruction (closes #426)#434
Conversation
Market data rides the dedicated KalshiMD session (port 8233, non-retransmission) and is identical on both products — only the price units differ (prediction dollars vs margin fixed-point dollars under UseDollars), and both ride the FIX Price field, so DollarDecimal parses either without float drift. NB: the bundled FIX dictionary v1.03 predates market data, so the message layouts were derived from docs.kalshi.com/fix/market-data (+ the margin variant), which carry the authoritative tag tables and wire examples. Messages (kalshi/fix/messages/market_data.py): - MarketDataRequest (35=V, outbound) with snapshot()/subscribe()/unsubscribe()/ unsubscribe_all() constructors that encode the 263/146/55 rules (cancel-all omits NoRelatedSym/Symbol) - SecurityStatusRequest (35=e, outbound) with subscribe()/unsubscribe() - MarketDataSnapshotFullRefresh (35=W), MarketDataIncrementalRefresh (35=X), MarketDataRequestReject (35=Y), SecurityStatus (35=f) inbound; code fields kept raw (compare against enums), matching the order-entry convention - new enums: SubscriptionRequestType, MDEntryType, MDUpdateAction, MDReqRejReason, SecurityTradingStatus; new market-data tags Order-book reconstruction (kalshi/fix/orderbook.py): - FixOrderBook builds aggregated bid/offer books from a W snapshot + X incrementals (absolute sizes: Change sets, Delete removes). Snapshot replaces a market's book; incrementals route per-entry by Symbol; an incremental for an un-seeded market is dropped. clear()/remove() support reconnect re-subscribe (KalshiMD has no retransmission, so a gap reconnects and re-snapshots). Inbound dispatch registers W/X/Y/f. The client.market_data() session factory and the MD host/port/non-retransmission config already existed from the foundation. Hardening from a pre-PR adversarial review: apply_incremental drops an unknown MDUpdateAction instead of applying it as a Change; apply_snapshot drops 0-size levels (parity with Delete); the per-market snapshot-replacement contract (caller must clear() before re-subscribing post-gap) is documented on the public API. 34 new tests: golden exact-wire fixtures + round-trips for all six messages, dispatch, order-book snapshot/Change/Delete/zero-size/empty-snapshot/unknown- action/before-snapshot/symbol-routing/stale-survival, a KalshiMD subscription integration test, and a drop-connection gap-recovery integration test (both against the mock acceptor). ruff + mypy --strict clean; 165 FIX tests pass. Closes #426. Part of #402. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #434: FIX market-data session + order-book reconstructionOverviewWell-structured addition of FIX market-data support (messages V/W/X/Y/e/f), a clean Bug:
|
- orderbook.apply_incremental: check MDUpdateAction BEFORE the size guard. An unknown action carrying size 0 was hitting the leading `size <= 0 -> pop` branch and silently deleting the level instead of being dropped — the same silent-mutation class the unknown-action branch was added to prevent, just via Delete. Now: Delete -> pop; Change -> (size<=0 ? pop : set); unknown -> drop. Regression test parametrized over size 5 and 0. - Trim the market_data.py module docstring to match the sibling FIX modules (drop the per-message table + code block; the PR/docs carry that). Not changed, with rationale: - MDReqID (262): intentionally omitted, now documented on MarketDataRequest. The Kalshi MD docs do not define tag 262 on 35=V/W/X/Y (0 occurrences in the dictionary; the docs state subscriptions/rejects correlate by Symbol<55>), unlike generic FIX 5.0SP2. Adding it would emit an undefined tag. (FIX messages are not part of METHOD_ENDPOINT_MAP, so the EXCLUSIONS mechanism does not apply.) - f-string prefix nit: false positive — TICKER is interpolated, so the f-prefix is required; ruff (F541) is clean. ruff + mypy --strict clean; 166 FIX tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — feat(fix): market-data session + order-book reconstructionOverall this is high-quality work. The design is clean, the pre-PR adversarial-review hardening shows (the unknown-action / 0-size-snapshot paths are explicitly tested), and the golden wire fixtures are the right tool for catching field-order / repeating-group drift. Comments below focus on the things that actually matter. Potential Bug / Fragility
class MDIncrementalEntry(FixGroup):
md_update_action: str = fixfield(Tag.MD_UPDATE_ACTION, FixType.CHAR)
symbol: str = fixfield(Tag.SYMBOL, FixType.STRING)
md_entry_type: str = fixfield(Tag.MD_ENTRY_TYPE, FixType.CHAR)
md_entry_px: DollarDecimal = fixfield(Tag.MD_ENTRY_PX, FixType.PRICE) # required
md_entry_size: FixedPointCount = fixfield(Tag.MD_ENTRY_SIZE, FixType.QTY) # requiredStandard FIX 5.0SP2 allows The PR description says md_entry_px: DollarDecimal | None = fixfield(Tag.MD_ENTRY_PX, FixType.PRICE, default=None)
md_entry_size: FixedPointCount | None = fixfield(Tag.MD_ENTRY_SIZE, FixType.QTY, default=None)
Convention Violation (CLAUDE.md)Issue references in module docstrings
# orderbook.py line 1
"""Local order-book reconstruction from FIX market data (GH #426).
# tests/fix/test_market_data.py line 1
"""Tests for the market-data FIX flow + order-book reconstruction (GH #426)."""Drop the Minor
Standard FIX defines class MDUpdateAction(StrEnum):
"""Tag 279 — action for an incremental market-data entry (35=X).
FIX 5.0SP2 also defines New=0; Kalshi MD does not emit it, so it is
intentionally absent — an action="0" will be dropped as unknown.
"""
CHANGE = "1"
DELETE = "2"
# uses .value (string comparison)
assert body[int(Tag.SUBSCRIPTION_REQUEST_TYPE)] == SubscriptionRequestType.DISABLE.value
# neighboring test uses enum directly
assert body[int(Tag.SUBSCRIPTION_REQUEST_TYPE)] == SubscriptionRequestType.SNAPSHOT_PLUS_UPDATESPick one style consistently (enum direct comparison is cleaner). Missing golden wire test for
assert _wire(MarketDataRequest.subscribe([TICKER])) == f"263=1|146=1|55={TICKER}"Design Note (not blocking)
What's excellent
Priority of the above: the |
- MDSnapshotEntry / MDIncrementalEntry: make md_entry_px / md_entry_size optional (default=None), matching the order-entry inbound convention (parse a slightly off-spec server entry rather than rejecting the whole message). FIX permits a Delete to omit MDEntryPx/MDEntrySize; the parser now tolerates it. - orderbook: handle the now-optional fields — apply_snapshot skips a level lacking price or positive size; apply_incremental drops an entry without MDEntryPx, and a Change with absent/0 size clears the level. Regression test decodes an X Delete that omits tag 271 and confirms it parses + removes. - enums: document that MDUpdateAction intentionally omits FIX New=0 (Kalshi MD doesn't emit it; action='0' is dropped as unknown). - tests: add the missing subscribe (263=1) golden-wire assertion; make the unsubscribe assertions enum-direct (drop .value) for consistency. Not changed, with rationale: - (GH #426) module-docstring tags: kept. The cited CLAUDE.md rule is not present in the repo's CLAUDE.md/AGENTS.md, and all sibling FIX modules (order_entry #424, drop_copy #425, components #423) carry the same provenance tag. - to_view() per-get sort: left as-is (books are tiny; no speculative cache), consistent with the reviewer flagging it as non-blocking. ruff + mypy --strict clean; 167 FIX tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #434: FIX market-data session + order-book reconstructionOverall verdict: looks solid. The design is clean, the test coverage is thorough, and the edge-case handling is clearly thought-through. A few findings below, roughly in priority order. Findings1.
|
Summary
FIX Market Data on the dedicated
KalshiMDsession — order-book snapshots/incrementals and per-market trading-status — for both products. Builds on the foundation (#422), repeating groups (#423), order entry (#424), and drop copy (#425).Messages (
messages/market_data.py)MarketDataRequestsnapshot()/subscribe()/unsubscribe()/unsubscribe_all()constructors encode the263/146/55rules (cancel-all omitsNoRelatedSym/Symbol)SecurityStatusRequestsubscribe()/unsubscribe()a single marketMarketDataSnapshotFullRefreshSymbol+NoMDEntriesgroup{type,px,size}MarketDataIncrementalRefreshNoMDEntriesgroup{action,symbol,type,px,size}(per-entry symbol)MarketDataRequestRejectmd_req_rej_reason+textSecurityStatussymbol+security_trading_statusInbound code fields stay raw (
str/int, compare against the enums) — matching the order-entry convention so a slightly off-spec message still parses. New enums:SubscriptionRequestType,MDEntryType,MDUpdateAction,MDReqRejReason,SecurityTradingStatus. Prices useDollarDecimal(dollars on both products; margin enforcesUseDollars).Order-book reconstruction (
orderbook.py)FixOrderBookbuilds aggregated bid/offer books from a W snapshot + X incrementals. FIX MD uses absolute level sizes (not signed deltas):Changesets a level's new size,Deleteremoves it. A snapshot replaces a market's book; incrementals route per-entry bySymbol; an incremental for an un-seeded market is dropped.clear()/remove()support the reconnect→re-subscribe path (KalshiMDhas no retransmission, so a sequence gap tears the session down and the client re-snapshots).The
client.market_data()session factory and the MD host-split / port-8233 / non-retransmission config already existed from the foundation.Pre-PR adversarial review hardening
A multi-agent review (4 lenses → per-finding skeptical verification) ran before this PR; the confirmed findings are folded in:
apply_incrementaldrops an unknownMDUpdateActioninstead of silently applying it as a Change (was a robustness gap vs the sibling unknown-MDEntryTypepath).apply_snapshotdrops 0-size levels — parity with an incrementalDelete, so "size 0 = no level" holds on both paths.clear()before re-subscribing after a gap/teardown, else markets not re-seeded keep stale books.Tests (
tests/fix/test_market_data.py, 34 new)Golden exact-wire fixtures (pinned to the doc examples — they catch field-order / repeating-group-layout drift that round-trips can't) + structural round-trips for all six messages; dispatch; order-book snapshot ordering, Change/Delete, change-to-zero, 0-size-snapshot drop, empty-snapshot clears book, unknown-action drop, incremental-before-snapshot, per-symbol routing, stale-survival contract; a
KalshiMDsubscription integration test (subscribe → W → X → reconstruct); and a drop-connection gap-recovery integration test (reconnect →clear()→ re-subscribe → fresh W rebuilds) — both against the mock acceptor.Verification
uv run ruff check .— cleanuv run mypy kalshi/(strict) — clean (154 files)uv run pytest tests/fix/— 165 passedCloses #426. Part of #402.
🤖 Generated with Claude Code