FIX market settlement — prediction only. A read-only MarketSettlementReport
(35=UMS) stream on KalshiPT (default on) and on KalshiRT (opt in via
ReceiveSettlementReports). This is the last sub-issue of the #402 FIX epic.
- messages/settlement.py: MarketSettlementReport (UMS), inbound (fields optional,
codes raw). Reuses the MarketSettlementParty group entry (from #423), which
nests NoCollateralAmountChanges + NoMiscFees per party. market_result is a raw
str (compare against the new MarketResult enum); settlement_price (cents) and
collateral/fee amounts are DollarDecimal; positions FixedPointCount.
- enums.py: MarketResult (yes/no/scalar), CollateralAmountType (BALANCE/PAYOUT).
- settlement.py: SettlementReassembler — paginated batches span multiple UMS
fragments whose MarketSettlementReportID differs per page, so it correlates by
Symbol and accumulates parties until the terminal fragment (LastFragment Y/
absent), emitting one combined report.
- config: receive_settlement_reports (bool | None) threaded into the Logon — None
omits tag 20127 (gateway default: on for PT, off for RT), True opts RT in
(20127=Y), False opts PT out (20127=N). The Logon field + all tags already
existed from the foundation; client.post_trade() is the session factory.
- dispatch registers UMS; exports from kalshi.fix / kalshi.fix.messages.
Hardening from a pre-PR adversarial review (8 confirmed findings; the money path
and nested decode were verified correct). The reassembler keys only on Symbol
(the report id differs per page) with no batch-id in the protocol, so its
contiguous-per-symbol-batch assumption + limitations (no TotNum reconciliation;
interleaved same-symbol batches would coalesce — not produced by a well-formed
stream; orphaned buffers persist until clear(); duplicate terminals re-emit) are
now documented on the class, and clear() warns when it discards non-empty
buffers. Filed #437 for a FIX dictionary<->model drift test (infra follow-up).
17 new tests: golden UMS wire, nested-group round-trip (incl. two parties each
with collateral + fee groups, negative collateral, sub-cent fee), a decode of the
docs example with LastFragment trailing the party group, dispatch, multi-fragment
reassembly (buffer/emit/per-symbol/standalone/no-symbol/sequential-same-symbol/
clear-warns), a KalshiPT settlement-stream integration test, and the
ReceiveSettlementReports logon flag across opt-in / opt-out / default.
ruff + mypy --strict clean; 248 FIX tests pass.
Closes #429. Part of #402.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
FIX Market Settlement / Post-Trade — prediction only, and the final sub-issue of the #402 epic. A read-only
MarketSettlementReport(35=UMS) stream on theKalshiPTsession (settlement on by default) and onKalshiRTwhen opted in. Builds on the foundation (#422) and the nested repeating-group support (#423).Messages (
messages/settlement.py)MarketSettlementReport(35=UMS), inbound — fields optional, codes raw (the inbound convention). It reuses the existingMarketSettlementPartygroup entry (built in #423), which nestsNoCollateralAmountChangesandNoMiscFeesper party. Money fields:settlement_price(cents, e.g.100.00), collateral/fee amounts →DollarDecimal;LongQty/ShortQty→FixedPointCount(no float). New enums:MarketResult(yes/no/scalar),CollateralAmountType(BALANCE/PAYOUT).Multi-fragment reassembly (
settlement.py)Large settlement batches paginate across several UMS fragments. The
MarketSettlementReportIDdiffers per page, soSettlementReassemblercorrelates fragments bySymbol, accumulates each symbol's party entries, and emits one combinedMarketSettlementReporton the terminal fragment (LastFragment=Yor absent;=Nmeans more pages). Per-symbol isolation handles interleaved batches; a standalone report passes through unchanged.ReceiveSettlementReports(tag 20127)FixConfig.receive_settlement_reportsisbool | None, passed straight to the Logon:Noneomits the tag (gateway default — on for KalshiPT, off for KalshiRT),TrueoptsKalshiRTin (20127=Y),FalseoptsKalshiPTout (20127=N). The Logon field, all settlement tags, andclient.post_trade()already existed from the foundation.Pre-PR adversarial review
A multi-agent review (4 lenses → per-finding skeptical verification, 8 confirmed) ran against this branch. It verified the money path and nested-group decode are correct (field/type faithfulness, two-party nested decode with no cross-contamination, negative collateral + sub-cent fees round-trip exact). Folded in:
Symbol(the report id differs per page) and the protocol has no batch-id, so its contiguous-per-symbol-batch assumption + limitations are now documented on the class: noTotNumreconciliation; two interleaved same-symbol batches would coalesce (a well-formed stream sends a symbol's fragments contiguously, so it doesn't occur — sequential same-symbol batches reassemble independently); orphaned buffers persist untilclear(); duplicate terminals re-emit (consumers handle idempotently).clear()now warns when discarding non-empty buffers (observability for dropped partial settlement data).Tests (
tests/fix/test_settlement.py, 17 new)Golden UMS wire; nested-group round-trip including two parties each with their own collateral + fee groups (negative collateral, sub-cent fee); a decode of the docs example with
LastFragmenttrailing the party group (proves the positional nested decode); dispatch; multi-fragment reassembly (buffer / emit / per-symbol / standalone / no-symbol / sequential same-symbol / clear-warns); aKalshiPTsettlement-stream integration test against the mock acceptor; and theReceiveSettlementReportslogon flag across opt-in / opt-out / default.Verification
uv run ruff check .— cleanuv run mypy kalshi/(strict) — clean (158 files)uv run pytest tests/fix/— 245 passedCloses #429. Part of #402.
🤖 Generated with Claude Code