From 19aaff6533f41c5bb692c50d7933376caaed8951 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sat, 6 Jun 2026 10:45:23 -0500 Subject: [PATCH 1/2] docs: FIX documentation + accuracy pass + release prep for 3.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump to 3.3.0 (pyproject + __init__) for the additive FIX subsystem (#402) and the decode-routing (#432) and drift-test (#437) follow-ups. No breaking changes. FIX documentation (the subsystem was entirely undocumented): - new docs/fix.md guide; FIX section in README; FIX in the docs landing page features + "where to go next" + mkdocs nav; FIX error hierarchy in errors.md; FIX reference autodoc in reference.md; FIX cross-references in authentication / configuration / environment-variables / perps / resources index. - re-export the FIX entry points (FixClient, MarginFixClient, FixConfig, FixEnvironment, FixSessionType) from the top-level kalshi package (consistent with PerpsClient); refresh the stale kalshi.fix package docstring. Documentation accuracy fixes (from a multi-agent docs audit): - fcm.md: document fcm.positions_all() (the page wrongly said it did not exist). - portfolio.md: document the subaccount parameter on the read methods. - markets.md: is_block_trade version corrected to SDK v3.1.0 (was v3.20.0 — the OpenAPI spec version, not the SDK version). - index.md: WebSocket channel count corrected to 11 (matches websockets.md). - types.md: document OrderPrice and MultiplierDecimal. - environment-variables.md: the key passphrase is read by from_env() too. Release metadata: CHANGELOG + ROADMAP 3.3.0 entries; CHANGELOG 3.1.0 date aligned to 2026-06-04 (matching the release tag / ROADMAP). ruff + mypy --strict clean; mkdocs build --strict clean; 4194 tests pass (the 4 from_env failures are the pre-existing full-run env-leak flake — they pass in isolation). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 43 ++++++- README.md | 36 ++++++ ROADMAP.md | 7 ++ docs/authentication.md | 7 ++ docs/configuration.md | 7 ++ docs/environment-variables.md | 12 +- docs/errors.md | 41 +++++++ docs/fix.md | 210 ++++++++++++++++++++++++++++++++++ docs/index.md | 8 +- docs/perps.md | 8 ++ docs/reference.md | 20 ++++ docs/resources/fcm.md | 13 ++- docs/resources/index.md | 4 + docs/resources/markets.md | 5 +- docs/resources/portfolio.md | 5 + docs/types.md | 18 +++ kalshi/__init__.py | 9 +- kalshi/fix/__init__.py | 11 +- mkdocs.yml | 1 + pyproject.toml | 2 +- 20 files changed, 453 insertions(+), 14 deletions(-) create mode 100644 docs/fix.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c62e71..4129f8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ All notable changes to kalshi-sdk will be documented in this file. +## 3.3.0 — 2026-06-06 + +Adds a complete **FIX protocol** subsystem (FIXT.1.1 / FIX50SP2) for both the +prediction and perps products. Additive release: no changes to the existing REST, +WebSocket, or Perps surfaces. + +### Added + +- **FIX engine (`kalshi.fix`)** — a hand-rolled, async-first FIX client for both + products: `FixClient` (prediction) and `MarginFixClient` (margin, in + `kalshi.perps.fix`), re-exported from the top-level `kalshi` package alongside + `FixConfig`, `FixEnvironment`, and `FixSessionType`. Covers six session types — + order entry (NR/RT), drop copy, market data, order groups, post-trade + settlement (prediction), and RFQ (prediction) — on a session state machine with + RSA-PSS logon (reusing the REST key), heartbeat / test-request liveness, + sequence tracking with gap-fill / resend on the retransmission sessions, and + AWS-style full-jitter reconnect. (Epic #402.) +- **Typed FIX messages** — Pydantic v2 models for every Kalshi FIX message across + order entry, order groups, market data, RFQ/quoting, drop copy, and market + settlement, with `DollarDecimal` / `FixedPointCount` money types (no float + drift) and a central inbound dispatch via `decode_app_message`. +- **`FixOrderBook`** — aggregated order-book reconstruction from market-data + snapshots + incrementals; **`SettlementReassembler`** — paginated + settlement-report reassembly. +- **`FixSession.on_decode_error`** hook + `decode_app_message_strict` — surface a + registered-but-malformed inbound message instead of silently dropping it (#432). +- **FIX documentation** — new `docs/fix.md` guide, plus FIX coverage in the + README, errors, authentication, configuration, environment-variables, and the + API reference. +- **FIX dictionary drift test** — `specs/kalshi-fix-dictionary.xml` checked in + with a model↔dictionary drift test, the FIX analogue of the REST contract-drift + suite (#437). + +### Fixed + +- Documentation accuracy pass: `fcm.positions_all()` is now documented (the FCM + page previously stated it did not exist); the `portfolio` reads document their + `subaccount` parameter; the `markets.is_block_trade` version note is corrected + to SDK v3.1.0; the `OrderPrice` and `MultiplierDecimal` types are documented; + and the docs landing page's WebSocket channel count is corrected to 11. + ## 3.2.0 — 2026-06-05 Adds full SDK support for the Kalshi **Perps (margin) API** — a separate @@ -81,7 +122,7 @@ prediction-API surface. Additive release: no changes to `KalshiClient`. - README / `docs/index.md` banners note the perps surface (34 REST operations, 6 WS channels, 10 SCM operations). -## 3.1.0 — 2026-06-03 +## 3.1.0 — 2026-06-04 OpenAPI + AsyncAPI spec sync from v3.19.0 → v3.20.0 (`#385`). Adds the new `GET /events/fee_changes` endpoint plus three smaller additive diff --git a/README.md b/README.md index 29521def..06c6b5e5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi - **Full coverage** of the Kalshi REST API (99 operations across 19 resources, OpenAPI v3.20.0) and WebSocket API (11 typed `subscribe_*` channels + 2 escape-hatch). - **Perps (margin) API**: standalone `PerpsClient` / `AsyncPerpsClient` + `PerpsWebSocket` for the perpetual-futures exchange (34 REST operations, 6 WS channels), plus a `KlearClient` for the Self-Clearing-Member "Klear" settlement API (10 operations). See [Perps (margin) trading](#perps-margin-trading). +- **FIX protocol**: an async-first FIX engine (FIXT.1.1 / FIX50SP2) for both products — order-entry, drop-copy, market-data, order-groups, post-trade (prediction), and RFQ (prediction) sessions with typed message models, sequence recovery, and order-book / settlement reassembly. `from kalshi import FixClient` / `MarginFixClient`. See [FIX protocol](#fix-protocol-low-latency-trading). - **V2 event-market orders**: `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` plus batched variants on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` keeps working — deprecated no earlier than May 6, 2026. - **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`. - **Sync and async** clients sharing one transport — no thread-pool wrapping. @@ -250,6 +251,41 @@ obligations, withdrawals) is a third surface exposed via `KlearClient`, which us **cookie-session + MFA** login (`client.login(email=..., password=..., code=...)`) rather than RSA-PSS. Full guide: [docs/perps.md](docs/perps.md). +## FIX protocol (low-latency trading) + +For persistent, low-latency sessions the SDK includes a hand-rolled, **async-first** +FIX engine (FIXT.1.1 / FIX50SP2) for both products: order-entry, drop-copy, +market-data, order-groups, post-trade (prediction), and RFQ (prediction) sessions +with typed message models, automatic logon/heartbeat/sequence recovery, and +order-book / settlement reassembly. It reuses the same RSA-PSS key as the REST +client (`KALSHI_*` for prediction, `KALSHI_PERPS_*` for margin). + +```python +import asyncio +from decimal import Decimal +from kalshi import FixClient, FixEnvironment +from kalshi.fix import NewOrderSingle, ExecutionReport, Side, decode_app_message + +async def main() -> None: + async def on_message(raw) -> None: + msg = decode_app_message(raw) + if isinstance(msg, ExecutionReport): + print(msg.cl_ord_id, msg.exec_type, msg.ord_status) + + client = FixClient.from_env(environment=FixEnvironment.DEMO) + async with client.order_entry(on_message=on_message) as session: + await session.send(NewOrderSingle( + cl_ord_id="order-1", symbol="KXNBAGAME-26MAY25NYKCLE-NYK", + side=Side.BUY_YES, order_qty=Decimal("10"), price=Decimal("0.55"), + )) + await asyncio.sleep(2) + +asyncio.run(main()) +``` + +Prediction uses `FixClient`; margin uses `MarginFixClient`. Full guide: +[docs/fix.md](docs/fix.md). + ## Error handling All SDK errors inherit from `KalshiError`: diff --git a/ROADMAP.md b/ROADMAP.md index ee8b5ff4..1c858e24 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,6 +2,13 @@ ## Shipped +- **v3.3.0 (2026-06-06)** — complete **FIX protocol** subsystem (FIXT.1.1 / + FIX50SP2) for both products: `FixClient` (prediction) + `MarginFixClient` + (margin) over a hand-rolled async session engine, with six session types + (order entry, drop copy, market data, order groups, post-trade settlement, + RFQ), typed message models, order-book / settlement reassembly, and a + model↔dictionary drift test (epic #402; follow-ups #432, #437). New + [FIX guide](docs/fix.md). Additive — no changes to REST / WebSocket / Perps. - **v3.2.0 (2026-06-05)** — full **Perps (margin) API** support: standalone `PerpsClient` / `AsyncPerpsClient` (REST), `PerpsWebSocket`, and the Self-Clearing-Member `KlearClient` (cookie-session + MFA settlement API), diff --git a/docs/authentication.md b/docs/authentication.md index 2036eb41..48349668 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -284,4 +284,11 @@ API is unchanged for sync-transport callers. directly if you construct `KalshiAuth` standalone (e.g. for the WebSocket with no REST client alongside). +## FIX authentication + +The [FIX subsystem](fix.md) reuses this same RSA-PSS key — the logon signature +rides the FIX `RawData` field. `FixClient` reads the same `KALSHI_*` variables (or +`from_auth(auth)` reuses an existing `KalshiAuth`); the margin `MarginFixClient` +uses the separate `KALSHI_PERPS_*` key. No FIX-specific credentials are needed. + See the [API reference](reference.md) for the full surface. diff --git a/docs/configuration.md b/docs/configuration.md index e8edf3af..695fb66b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -178,6 +178,13 @@ async with AsyncKalshiClient.from_env() as client: ... ``` +## FIX connectivity + +The [FIX subsystem](fix.md) has its own `FixConfig` (host/port per product + +environment + session type, TLS, heartbeat, retry policy, and FIX logon options). +It is independent of `KalshiConfig`; see [FIX → Connectivity & +configuration](fix.md#connectivity-configuration). + ## Reference ::: kalshi.config.KalshiConfig diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 4e9442d4..2c48c0aa 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -8,7 +8,7 @@ these: | `KALSHI_KEY_ID` | unset | Key ID. If unset, `from_env()` returns an unauthenticated client. | | `KALSHI_PRIVATE_KEY` | unset | PEM string. **Conflicts** with `KALSHI_PRIVATE_KEY_PATH` — set exactly one. | | `KALSHI_PRIVATE_KEY_PATH` | unset | Path to PEM file. `~` is expanded. Conflicts with `KALSHI_PRIVATE_KEY`. | -| `KALSHI_PRIVATE_KEY_PASSPHRASE` | unset | Passphrase for an encrypted PEM (read by `try_from_env()`). | +| `KALSHI_PRIVATE_KEY_PASSPHRASE` | unset | Passphrase for an encrypted PEM (read by `from_env()` / `try_from_env()`, and by `FixClient.from_env()`). | | `KALSHI_DEMO` | `false` | The exact string `true` (case-insensitive) selects the demo URLs. Other values (`1`, `yes`, `on`) are **not** recognized. | | `KALSHI_API_BASE_URL` | unset | Overrides `base_url` entirely. Wins over `KALSHI_DEMO`. | | `KALSHI_ALLOW_UNKNOWN_HOST` | unset | Set to `1` to allow `base_url`/`ws_base_url` hosts outside `{api.elections.kalshi.com, demo-api.kalshi.co, localhost}`. Default-fail prevents typos like `kalsi.com` from silently signing to an attacker (#250). | @@ -80,3 +80,13 @@ read only environment selectors: | `KALSHI_KLEAR_DEMO` | `false` | `true` (case-insensitive) selects the demo Klear endpoint. | | `KALSHI_KLEAR_API_BASE_URL` | unset | Overrides the Klear `base_url`. | | `KALSHI_KLEAR_ALLOW_UNKNOWN_HOST` | unset | Set to `1` to allow non-Kalshi Klear hosts. | + +## FIX + +The [FIX subsystem](fix.md) reuses the credential variables above: +`FixClient.from_env()` reads the prediction `KALSHI_KEY_ID` / +`KALSHI_PRIVATE_KEY[_PATH]` / `KALSHI_PRIVATE_KEY_PASSPHRASE`, and +`MarginFixClient.from_env()` reads the perps `KALSHI_PERPS_*` set. FIX host/port is +resolved by `FixConfig` (not an env var); use a `host=`/`port=` override plus +`allow_unknown_host=True` or `KALSHI_FIX_ALLOW_UNKNOWN_HOST=1` only for a mock or +local TLS proxy. diff --git a/docs/errors.md b/docs/errors.md index 0624c64c..a8c2774a 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -162,6 +162,31 @@ client-side id. You won't see `KalshiConnectionError` from inside `async for`; you'll see it from the `connect()` context manager if the socket can't be re-established at all. +## FIX errors + +The [FIX subsystem](fix.md) has its own sub-hierarchy under `KalshiFixError` +(itself a subclass of `KalshiError`, so `except KalshiError` still catches it). +Import them from `kalshi.fix`. FIX is a TCP/TLS protocol with no HTTP status, so +`status_code` is always `None`. + +- **`FixConnectionError`** — TCP/TLS connect failed, was refused, or the + reconnect attempts were exhausted. Original transport error via `__cause__`. +- **`FixLogonError`** — the gateway rejected the logon; `.reason` carries the + `Text` from the Logout when present (bad signature, CompID, SendingTime skew, a + missing `ResetSeqNumFlag=Y`). +- **`FixSequenceError`** — an unrecoverable sequence condition (a backwards + `MsgSeqNum`, or a forward gap on a non-retransmission session); carries + `.expected` / `.received`. +- **`FixCodecError`** — a malformed frame (`BeginString` / `BodyLength` / + `CheckSum` / `tag=value`); `.raw` holds the offending bytes when available. +- **`FixDecodeError`** — a *registered* inbound message failed schema validation + (one off-spec field); carries `.raw` + `.msg_type`, original via `__cause__`. + See [FIX → Error handling](fix.md#error-handling). +- **`FixRejectError`** — the gateway rejected a message we sent (session Reject + 35=3 or BusinessMessageReject 35=j); carries the structured reject fields. +- **`FixSessionError`** — a session-level protocol violation or unexpected + lifecycle event. + ## See also - [Retries & idempotency](retries.md) — what does and doesn't get retried, why @@ -202,3 +227,19 @@ re-established at all. ::: kalshi.errors.KalshiOrderbookUnavailableError ::: kalshi.errors.KalshiSubscriptionError + +::: kalshi.fix.KalshiFixError + +::: kalshi.fix.FixConnectionError + +::: kalshi.fix.FixLogonError + +::: kalshi.fix.FixSequenceError + +::: kalshi.fix.FixCodecError + +::: kalshi.fix.FixDecodeError + +::: kalshi.fix.FixRejectError + +::: kalshi.fix.FixSessionError diff --git a/docs/fix.md b/docs/fix.md new file mode 100644 index 00000000..b8523bf4 --- /dev/null +++ b/docs/fix.md @@ -0,0 +1,210 @@ +# FIX protocol + +The SDK ships a hand-rolled, **async-first** FIX engine (FIXT.1.1 transport / +FIX50SP2 application layer) for both Kalshi products — the prediction +(event-contract) exchange and the perps (margin) exchange. It speaks Kalshi's FIX +dialect directly: tag=value framing, typed Pydantic message models, an +RSA-PSS logon, and a session state machine with heartbeats, sequence tracking, +and automatic reconnect. + +Use FIX when you want a persistent, low-latency session for order entry, fills, +market data, or settlement. For everything else — most REST endpoints, ad-hoc +queries — the [REST client](getting-started.md) is simpler; for streaming public +data without a FIX session, see [WebSocket](websockets.md). + +!!! note "Async only" + The FIX engine is built on `asyncio`; every session is an async context + manager. There is no sync facade. + +## Import + +The two client entry points are re-exported from the top-level package; the +message models, enums, errors, and helpers live in `kalshi.fix`: + +```python +from kalshi import FixClient, MarginFixClient, FixConfig, FixEnvironment, FixSessionType +from kalshi.fix import NewOrderSingle, ExecutionReport, decode_app_message, Side +``` + +## Authentication + +FIX reuses the same **RSA-PSS** key as the REST client (the signature rides the +logon's `RawData` field). Credentials come from the same environment variables: + +- **Prediction** (`FixClient`) — `KALSHI_KEY_ID` + `KALSHI_PRIVATE_KEY` (or + `KALSHI_PRIVATE_KEY_PATH`). Same key as the REST `KalshiClient`. +- **Margin** (`MarginFixClient`) — the separate `KALSHI_PERPS_*` namespace + (`KALSHI_PERPS_KEY_ID` + `KALSHI_PERPS_PRIVATE_KEY[_PATH]`). + +Both `from_env(...)` constructors accept `password=` (or read +`KALSHI_PRIVATE_KEY_PASSPHRASE`) to decrypt an encrypted key. You can also build a +client from an existing `KalshiAuth` via `from_auth(auth)`. + +```python +client = FixClient.from_env(environment=FixEnvironment.DEMO) +# or: FixClient.from_auth(auth, environment=FixEnvironment.DEMO) +``` + +## Sessions + +A FIX connection is a **session** of a specific type. Construct one with a +factory method on the client; it is an async context manager that logs on when +entered and logs out + disconnects when exited. + +| Factory | Session (TargetCompID) | Purpose | Prediction | Margin | +|---|---|---|:--:|:--:| +| `order_entry(retransmission=False)` | `KalshiNR` / `KalshiRT` | submit/cancel/amend orders, receive execution reports | ✅ | ✅ | +| `drop_copy()` | `KalshiDC` | replay historical execution reports | ✅ | ✅ | +| `market_data()` | `KalshiMD` | order-book snapshots/updates, security status | ✅ | ✅ | +| `post_trade()` | `KalshiPT` | market-settlement reports | ✅ | — | +| `rfq()` | `KalshiRFQ` | request-for-quote / market making | ✅ | — | + +Every factory takes the same callbacks: + +- `on_message(raw)` — each inbound **application** message as a raw + `RawMessage`; decode it with `decode_app_message(raw)`. +- `on_state_change(old, new)` — session-state transitions. +- `on_decode_error(raw, exc)` — *optional*; routes a registered-but-malformed + inbound message (see [Error handling](#error-handling)). + +The engine handles logon/heartbeat/test-request liveness, sequence tracking with +gap-fill/resend (on the retransmission sessions `KalshiRT`/`KalshiPT`), and +AWS-style full-jitter reconnect automatically. + +## Sending and receiving + +Send a typed message with `session.send(msg)` (returns the assigned +`MsgSeqNum`). Inbound application messages arrive on `on_message` as raw frames; +turn each into its typed model with `decode_app_message`: + +```python +import asyncio +from decimal import Decimal +from kalshi import FixClient, FixEnvironment +from kalshi.fix import NewOrderSingle, ExecutionReport, Side, decode_app_message + +async def main() -> None: + async def on_message(raw) -> None: + msg = decode_app_message(raw) + if isinstance(msg, ExecutionReport): + print("exec", msg.cl_ord_id, msg.exec_type, msg.ord_status, msg.avg_px) + + client = FixClient.from_env(environment=FixEnvironment.DEMO) + async with client.order_entry(on_message=on_message) as session: + await session.send( + NewOrderSingle( + cl_ord_id="my-order-1", + symbol="KXNBAGAME-26MAY25NYKCLE-NYK", + side=Side.BUY_YES, + order_qty=Decimal("10"), + price=Decimal("0.55"), # prediction: dollars or integer cents per UseDollars + ) + ) + await asyncio.sleep(2) # let execution reports arrive + +asyncio.run(main()) +``` + +The typed message families are exported from `kalshi.fix` (and +`kalshi.fix.messages`): order entry (`NewOrderSingle`, `OrderCancelRequest`, +`OrderCancelReplaceRequest`, `OrderMassCancelRequest`, `ExecutionReport`, +`OrderCancelReject`, …), order groups (`OrderGroupRequest`/`OrderGroupResponse`), +market data (`MarketDataRequest`, `MarketDataSnapshotFullRefresh`, +`MarketDataIncrementalRefresh`, `SecurityStatus`, …), RFQ/quoting (`QuoteRequest`, +`Quote`, `AcceptQuote`, `QuoteStatusReport`, …), and settlement +(`MarketSettlementReport`). Code fields on inbound messages are kept as raw +`str`/`int` — compare them against the enums in `kalshi.fix` (`ExecType`, +`OrdStatus`, `Side`, …). + +Prices use `Decimal` via `DollarDecimal` end-to-end (no float drift). On +prediction, order prices follow the session's `UseDollars` setting (integer cents +by default, dollars when enabled); on margin they are fixed-point dollars. + +## Market data and the order book + +`MarketDataRequest` helpers build subscribe/snapshot/unsubscribe requests, and +`FixOrderBook` reconstructs an aggregated book from a `W` snapshot plus `X` +incrementals: + +```python +from kalshi.fix import FixOrderBook, MarketDataRequest, decode_app_message + +book = FixOrderBook() +async with client.market_data(on_message=lambda raw: book.apply(decode_app_message(raw))) as md: + await md.send(MarketDataRequest.subscribe(["KXNBAGAME-26MAY25NYKCLE-NYK"])) + ... + view = book.get("KXNBAGAME-26MAY25NYKCLE-NYK") # MarketDataBook | None (bids/offers) +``` + +`KalshiMD` does not support retransmission — a gap tears the session down and the +client reconnects + re-subscribes; call `book.clear()` before re-subscribing. + +## Settlement (post-trade) + +Settlement reports stream on `KalshiPT` (on by default) and on `KalshiRT` when +opted in via `FixConfig.receive_settlement_reports=True`. Large batches paginate +across fragments correlated by `Symbol`; `SettlementReassembler` accumulates them +into one report: + +```python +from kalshi.fix import SettlementReassembler, decode_app_message + +reasm = SettlementReassembler() +async def on_message(raw): + complete = reasm.add(decode_app_message(raw)) # MarketSettlementReport | None until whole + if complete is not None: + ... # complete.parties has every party for the batch + +async with client.post_trade(on_message=on_message) as session: + ... +``` + +## Error handling + +FIX has its own exception family under `kalshi.fix` (all subclass `KalshiFixError`, +which subclasses the SDK-wide `KalshiError`): + +| Exception | Raised when | +|---|---| +| `FixConnectionError` | TCP/TLS connect failed or reconnect attempts exhausted | +| `FixLogonError` | the gateway rejected the logon (bad signature, CompID, seq) | +| `FixSequenceError` | an unrecoverable sequence condition (gap on a non-retransmission session) | +| `FixCodecError` | a malformed frame (BodyLength / CheckSum / framing) | +| `FixDecodeError` | a registered message failed schema validation (one off-spec field) | +| `FixRejectError` | the gateway rejected a message we sent (Reject / BusinessMessageReject) | +| `FixSessionError` | a session-level protocol violation or unexpected lifecycle event | + +`decode_app_message(raw)` returns `None` for both an unregistered message type +*and* a malformed known message. To distinguish them — so a genuine fill lost to a +single off-spec field is not silently dropped — set `on_decode_error`, which fires +with `(raw, FixDecodeError)` for the malformed case (the raw is still delivered to +`on_message`). `decode_app_message_strict(raw)` is the direct-call equivalent (it +raises `FixDecodeError` instead of returning `None`). + +```python +async def on_decode_error(raw, exc): # dead-letter / alert / halt + log.error("undecodable %s: %s", exc.msg_type, exc.__cause__) + +async with client.order_entry(on_message=handle, on_decode_error=on_decode_error) as s: + ... +``` + +## Connectivity & configuration + +`FixConfig.prediction(...)` / `FixConfig.margin(...)` resolve the host, port, and +session parameters per environment; pass `config=` to a client to override +defaults (heartbeat interval, connect timeout, retry policy, TLS, etc.). The +gateways require TLS to non-loopback hosts. A `host`/`port` override (e.g. a local +stunnel or a mock) is supported; a non-loopback host outside Kalshi's known FIX +endpoints must be opted into with `allow_unknown_host=True`. + +```python +config = FixConfig.prediction(environment=FixEnvironment.DEMO, heartbeat_interval=10) +client = FixClient.from_env(config=config) +``` + +## Reference + +Full public surface (clients, config, sessions, message models, enums, errors, +`FixOrderBook`, `SettlementReassembler`) is exported from `kalshi.fix`; see +[Reference](reference.md#fix-protocol). diff --git a/docs/index.md b/docs/index.md index 1b131a10..796914cd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ markets API. keeps working; deprecation no earlier than May 6, 2026. - **Funding + cost introspection** — `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`. -- **Full WebSocket coverage** — 13 channels with sequence-gap detection, automatic +- **Full WebSocket coverage** — 11 channels with sequence-gap detection, automatic reconnection (with resubscribe-window frame stashing for high-volume channels), backpressure strategies, and an in-memory orderbook builder. Async-only — access via `AsyncKalshiClient.ws`. @@ -18,6 +18,11 @@ markets API. `PerpsWebSocket` for the perpetual-futures exchange (34 REST operations, 6 WS channels), and a `KlearClient` for the Self-Clearing-Member settlement API (10 operations, cookie-session + MFA auth). See [Perps](perps.md). +- **FIX protocol** — a hand-rolled, async-first FIX engine (FIXT.1.1 / FIX50SP2) + for both products: order-entry, drop-copy, market-data, order-groups, post-trade + (prediction), and RFQ (prediction) sessions with typed message models, automatic + sequence recovery, and order-book / settlement reassembly. `from kalshi import + FixClient` (prediction) / `MarginFixClient` (margin). See [FIX](fix.md). - **Sync and async parity** — `KalshiClient` and `AsyncKalshiClient` share one transport implementation; method names, kwargs, return types, and error behavior are identical. The one exception is WebSocket access, which is async-only. @@ -63,6 +68,7 @@ No credentials needed for most market data. To place orders, see | Tune timeouts, retries, HTTP/2 | [Configuration](configuration.md) | | Walk pages or convert to DataFrames | [Pagination](pagination.md) · [DataFrames](dataframes.md) | | Build a live-data app | [WebSocket](websockets.md) | +| Trade over FIX (low latency) | [FIX](fix.md) | | Catch the right exception | [Errors](errors.md) · [Retries & idempotency](retries.md) | | Find every method on a resource | [Resources](resources/index.md) | | Test your code without hitting the API | [Testing](testing.md) | diff --git a/docs/perps.md b/docs/perps.md index 43924a3d..d3f759ba 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -155,3 +155,11 @@ Money fields on the Klear margin schemas are integer **centicents** (`1 USD = `klear.margin.withdraw_settlement_balance(amount="500.00")` validates the amount as positive at construction (the single real-money write) before any request is sent. Credentials and the session cookie are never logged or shown in `repr()`. + +## Perps over FIX + +The margin product is also reachable over the FIX protocol via `MarginFixClient` +(`from kalshi import MarginFixClient`), which reuses the shared FIX core with the +`KALSHI_PERPS_*` key and fixed-point-dollar pricing. Margin supports the +order-entry, drop-copy, and market-data sessions (no RFQ / post-trade). See +[FIX protocol](fix.md). diff --git a/docs/reference.md b/docs/reference.md index aca43e5c..d916f578 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -134,6 +134,26 @@ See [WebSocket](websockets.md) for the narrative version. ::: kalshi.ws.models +## FIX protocol + +See [FIX protocol](fix.md) for the narrative version. + +::: kalshi.fix.client.FixClient + +::: kalshi.perps.fix.MarginFixClient + +::: kalshi.fix.config.FixConfig + +::: kalshi.fix.session.FixSession + +::: kalshi.fix.orderbook.FixOrderBook + +::: kalshi.fix.settlement.SettlementReassembler + +::: kalshi.fix.messages + +::: kalshi.fix.enums + ## Testing helpers See [Testing](testing.md) for the narrative version. diff --git a/docs/resources/fcm.md b/docs/resources/fcm.md index 3d7f6237..6cc31c3a 100644 --- a/docs/resources/fcm.md +++ b/docs/resources/fcm.md @@ -51,8 +51,17 @@ for mp in resp.market_positions: ``` `positions()` returns a `PositionsResponse` (same shape as -[`portfolio.positions`](portfolio.md#positions)), **not** a `Page`. There is -no `positions_all()` helper — walk it manually if you need cursor traversal. +[`portfolio.positions`](portfolio.md#positions)), **not** a `Page`. For cursor +traversal use `positions_all()`, which auto-paginates `/fcm/positions` and yields +each `MarketPosition` (it mirrors `portfolio.positions_all()` and takes the same +filters — `subtrader_id`, `ticker`, `event_ticker`, `count_filter`, +`settlement_status`, `limit`, `max_pages`): + +```python +for mp in client.fcm.positions_all(subtrader_id="st_alpha", settlement_status="unsettled"): + print(mp.ticker, mp.position) +# async: `async for mp in client.fcm.positions_all(...)` +``` `settlement_status` is the FCM-specific kwarg that does **not** exist on `portfolio.positions()`. diff --git a/docs/resources/index.md b/docs/resources/index.md index dd244793..237158a4 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -1,5 +1,9 @@ # Resources +These are the REST resources. For the streaming and session interfaces see +[WebSocket](../websockets.md), [FIX protocol](../fix.md), and +[Perps](../perps.md). + A **resource** in this SDK is a thin client over a group of related Kalshi endpoints. You reach each one as an attribute on the client: diff --git a/docs/resources/markets.md b/docs/resources/markets.md index 3f22a863..7e0e036a 100644 --- a/docs/resources/markets.md +++ b/docs/resources/markets.md @@ -129,8 +129,9 @@ for trade in page: print(trade.trade_id, trade.taker_side, trade.yes_price, trade.count, trade.is_block_trade) ``` -`is_block_trade` (v3.20.0) filters by whether a trade is a block trade. Omit it -to return all trades. Each `Trade` also carries an `is_block_trade: bool`. +`is_block_trade` (SDK v3.1.0; OpenAPI v3.20.0) filters by whether a trade is a +block trade. Omit it to return all trades. Each `Trade` also carries an +`is_block_trade: bool`. !!! warning "Deprecated since v3.0.0" `list_trades_all` is the legacy name; it still works but emits diff --git a/docs/resources/portfolio.md b/docs/resources/portfolio.md index d0f7830e..eca70bf2 100644 --- a/docs/resources/portfolio.md +++ b/docs/resources/portfolio.md @@ -15,6 +15,11 @@ Auth required throughout. | `deposits(*, limit, cursor)` / `deposits_all(*, limit, max_pages)` | `GET /portfolio/deposits` | | `withdrawals(*, limit, cursor)` / `withdrawals_all(*, limit, max_pages)` | `GET /portfolio/withdrawals` | +`balance()`, `positions()` / `positions_all()`, `settlements()` / +`settlements_all()`, and `fills()` / `fills_all()` all take an optional +`subaccount: int` to scope the read to a specific subaccount (omit for the +primary account). + ## Balance ```python diff --git a/docs/types.md b/docs/types.md index b044c589..29afa52e 100644 --- a/docs/types.md +++ b/docs/types.md @@ -37,6 +37,24 @@ Same machinery as `DollarDecimal`, used for count-like fields the API fixed-points (e.g. queue position, `count_fp`). You shouldn't need to touch it directly. +### `OrderPrice` + +Request-side dollar price fields (`CreateOrderRequest.yes_price` / `no_price`, +`AmendOrderRequest`, and the V2 order equivalents) use `OrderPrice` — the same +wire form and coercion as `DollarDecimal`, plus two construction-time guards: the +price must be **non-negative** and aligned to the **$0.0001 tick**. A negative or +sub-tick value (`Decimal("-0.65")`, `Decimal("0.12345")`) raises `ValueError` at +construction instead of round-tripping to a server `400`. Response-side price +fields (PnL, fees, settlements) keep plain `DollarDecimal`, where negatives are +legitimate. + +### `MultiplierDecimal` + +A `Decimal` for fee/rate multipliers (e.g. `Series.fee_multiplier` and the perps +exchange parameters). Same no-float-drift coercion as `DollarDecimal` — the wire +form is a JSON number coerced via `str(value)`, so binary-float artifacts never +reach a model. + ### Integer cents Some fields are plain `int` cents, **not** `DollarDecimal`: diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 3405df2f..ac42a106 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -23,6 +23,7 @@ KalshiValidationError, KalshiWebSocketError, ) +from kalshi.fix import FixClient, FixConfig, FixEnvironment, FixSessionType from kalshi.models import ( RFQ, AcceptQuoteRequest, @@ -171,6 +172,7 @@ Withdrawal, ) from kalshi.perps import AsyncPerpsClient, PerpsClient, PerpsConfig, PerpsWebSocket +from kalshi.perps.fix import MarginFixClient from kalshi.perps.klear import ( AsyncKlearClient, KlearAuth, @@ -254,6 +256,10 @@ "EventStatusLiteral", "ExchangeStatus", "Fill", + "FixClient", + "FixConfig", + "FixEnvironment", + "FixSessionType", "ForecastPercentilesPoint", "GenerateApiKeyRequest", "GenerateApiKeyResponse", @@ -310,6 +316,7 @@ "LookupTickersForMarketInMultivariateEventCollectionRequest", "LookupTickersResponse", "MaintenanceWindow", + "MarginFixClient", "Market", "MarketCandlesticks", "MarketMetadata", @@ -372,4 +379,4 @@ "Withdrawal", ] -__version__ = "3.2.0" +__version__ = "3.3.0" diff --git a/kalshi/fix/__init__.py b/kalshi/fix/__init__.py index f4be1206..feec8d8e 100644 --- a/kalshi/fix/__init__.py +++ b/kalshi/fix/__init__.py @@ -8,14 +8,15 @@ engine; shared core in ``kalshi.fix`` with a thin margin facade in ``kalshi.perps.fix``). -This package is the *foundation* layer — codec, typed message models, the -session/recovery state machine, the RSA-PSS logon signer, and connectivity -config. Order-entry, drop-copy, market-data, and RFQ/settlement message flows -land in later phases. +The subsystem is complete: codec, typed message models, the session/recovery +state machine, the RSA-PSS logon signer, connectivity config, and every message +flow — order entry, drop copy, market data, order groups, RFQ (prediction), and +market settlement (prediction). ``FixClient`` / ``FixConfig`` / ``FixEnvironment`` +/ ``FixSessionType`` are also re-exported from the top-level ``kalshi`` package. Usage:: - from kalshi.fix import FixClient, FixEnvironment, FixSessionType + from kalshi import FixClient, FixEnvironment client = FixClient.from_env(environment=FixEnvironment.DEMO) async with client.order_entry(on_message=handle) as session: diff --git a/mkdocs.yml b/mkdocs.yml index 741b6715..594004f2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,6 +101,7 @@ nav: - Incentive programs: resources/incentive-programs.md - Live data: resources/live-data.md - WebSocket: websockets.md + - FIX protocol: fix.md - Perps: perps.md - Testing: testing.md - Migration: diff --git a/pyproject.toml b/pyproject.toml index ff89fe64..8093f305 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "3.2.0" +version = "3.3.0" description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs" readme = "README.md" license = { text = "MIT" } From 61f6126fec2743d83279d1f0cbff6fae17bfbb9c Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sat, 6 Jun 2026 10:53:35 -0500 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20address=20#441=20review=20=E2=80=94?= =?UTF-8?q?=20FIX=20example=20correctness=20+=20session-count=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix.md settlement example: guard `decode_app_message` with `isinstance(msg, MarketSettlementReport)` — SettlementReassembler.add() expects a settlement report, so feeding it None / another message type would raise. - fix.md market-data example: replace the sync `lambda` on_message with an `async def` (the session awaits the callback; a sync callable fails at runtime) and note that on_message must be async. - Correct "six session types … order groups" → there is no order_groups() session factory; order groups are messages on the order-entry session. Now "five session types (order entry NR/RT, drop copy, market data, post-trade, RFQ) + order-group management over the order-entry session" across README, docs index, fix.md (table note), CHANGELOG, ROADMAP. (The fix.md sessions table was already correct with 5 rows.) - fix.md: note the illustrative `asyncio.sleep` should be an asyncio.Event in real code. Not changed: KALSHI_FIX_ALLOW_UNKNOWN_HOST is documented in prose in the FIX env section (a single FIX-only escape hatch; kept inline rather than a 1-row table). mkdocs build --strict clean; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 7 ++++--- README.md | 11 ++++++----- ROADMAP.md | 7 ++++--- docs/fix.md | 36 +++++++++++++++++++++++++++++------- docs/index.md | 9 +++++---- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4129f8d1..657705e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,10 @@ WebSocket, or Perps surfaces. - **FIX engine (`kalshi.fix`)** — a hand-rolled, async-first FIX client for both products: `FixClient` (prediction) and `MarginFixClient` (margin, in `kalshi.perps.fix`), re-exported from the top-level `kalshi` package alongside - `FixConfig`, `FixEnvironment`, and `FixSessionType`. Covers six session types — - order entry (NR/RT), drop copy, market data, order groups, post-trade - settlement (prediction), and RFQ (prediction) — on a session state machine with + `FixConfig`, `FixEnvironment`, and `FixSessionType`. Covers five session types — + order entry (NR/RT), drop copy, market data, post-trade settlement (prediction), + and RFQ (prediction), plus order-group management over the order-entry session — + on a session state machine with RSA-PSS logon (reusing the REST key), heartbeat / test-request liveness, sequence tracking with gap-fill / resend on the retransmission sessions, and AWS-style full-jitter reconnect. (Epic #402.) diff --git a/README.md b/README.md index 06c6b5e5..88f7d0ae 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi - **Full coverage** of the Kalshi REST API (99 operations across 19 resources, OpenAPI v3.20.0) and WebSocket API (11 typed `subscribe_*` channels + 2 escape-hatch). - **Perps (margin) API**: standalone `PerpsClient` / `AsyncPerpsClient` + `PerpsWebSocket` for the perpetual-futures exchange (34 REST operations, 6 WS channels), plus a `KlearClient` for the Self-Clearing-Member "Klear" settlement API (10 operations). See [Perps (margin) trading](#perps-margin-trading). -- **FIX protocol**: an async-first FIX engine (FIXT.1.1 / FIX50SP2) for both products — order-entry, drop-copy, market-data, order-groups, post-trade (prediction), and RFQ (prediction) sessions with typed message models, sequence recovery, and order-book / settlement reassembly. `from kalshi import FixClient` / `MarginFixClient`. See [FIX protocol](#fix-protocol-low-latency-trading). +- **FIX protocol**: an async-first FIX engine (FIXT.1.1 / FIX50SP2) for both products — order-entry, drop-copy, market-data, post-trade (prediction), and RFQ (prediction) sessions (plus order-group management over the order-entry session) with typed message models, sequence recovery, and order-book / settlement reassembly. `from kalshi import FixClient` / `MarginFixClient`. See [FIX protocol](#fix-protocol-low-latency-trading). - **V2 event-market orders**: `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` plus batched variants on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` keeps working — deprecated no earlier than May 6, 2026. - **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`. - **Sync and async** clients sharing one transport — no thread-pool wrapping. @@ -255,10 +255,11 @@ rather than RSA-PSS. Full guide: [docs/perps.md](docs/perps.md). For persistent, low-latency sessions the SDK includes a hand-rolled, **async-first** FIX engine (FIXT.1.1 / FIX50SP2) for both products: order-entry, drop-copy, -market-data, order-groups, post-trade (prediction), and RFQ (prediction) sessions -with typed message models, automatic logon/heartbeat/sequence recovery, and -order-book / settlement reassembly. It reuses the same RSA-PSS key as the REST -client (`KALSHI_*` for prediction, `KALSHI_PERPS_*` for margin). +market-data, post-trade (prediction), and RFQ (prediction) sessions — plus +order-group management over the order-entry session — with typed message models, +automatic logon/heartbeat/sequence recovery, and order-book / settlement +reassembly. It reuses the same RSA-PSS key as the REST client (`KALSHI_*` for +prediction, `KALSHI_PERPS_*` for margin). ```python import asyncio diff --git a/ROADMAP.md b/ROADMAP.md index 1c858e24..5653ec83 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,9 +4,10 @@ - **v3.3.0 (2026-06-06)** — complete **FIX protocol** subsystem (FIXT.1.1 / FIX50SP2) for both products: `FixClient` (prediction) + `MarginFixClient` - (margin) over a hand-rolled async session engine, with six session types - (order entry, drop copy, market data, order groups, post-trade settlement, - RFQ), typed message models, order-book / settlement reassembly, and a + (margin) over a hand-rolled async session engine, with five session types + (order entry, drop copy, market data, post-trade settlement, RFQ; order groups + ride the order-entry session), typed message models, order-book / settlement + reassembly, and a model↔dictionary drift test (epic #402; follow-ups #432, #437). New [FIX guide](docs/fix.md). Additive — no changes to REST / WebSocket / Perps. - **v3.2.0 (2026-06-05)** — full **Perps (margin) API** support: standalone diff --git a/docs/fix.md b/docs/fix.md index b8523bf4..85e169d0 100644 --- a/docs/fix.md +++ b/docs/fix.md @@ -59,6 +59,9 @@ entered and logs out + disconnects when exited. | `post_trade()` | `KalshiPT` | market-settlement reports | ✅ | — | | `rfq()` | `KalshiRFQ` | request-for-quote / market making | ✅ | — | +Order groups are **not** a separate session — `OrderGroupRequest` / +`OrderGroupResponse` are exchanged over the order-entry session. + Every factory takes the same callbacks: - `on_message(raw)` — each inbound **application** message as a raw @@ -100,11 +103,16 @@ async def main() -> None: price=Decimal("0.55"), # prediction: dollars or integer cents per UseDollars ) ) - await asyncio.sleep(2) # let execution reports arrive + await asyncio.sleep(2) # illustration only — see note below asyncio.run(main()) ``` +The `asyncio.sleep(2)` just keeps the session alive long enough for the example to +print; in real code coordinate on an `asyncio.Event` (or your own run loop) that +your `on_message` handler sets when it has seen what it's waiting for, rather than +a fixed delay. + The typed message families are exported from `kalshi.fix` (and `kalshi.fix.messages`): order entry (`NewOrderSingle`, `OrderCancelRequest`, `OrderCancelReplaceRequest`, `OrderMassCancelRequest`, `ExecutionReport`, @@ -126,11 +134,19 @@ by default, dollars when enabled); on margin they are fixed-point dollars. `FixOrderBook` reconstructs an aggregated book from a `W` snapshot plus `X` incrementals: +The `on_message` callback is awaited, so it must be an `async def` (a plain +`def`/`lambda` will fail). `FixOrderBook.apply()` accepts any decoded message and +ignores anything that isn't a market-data snapshot/incremental: + ```python from kalshi.fix import FixOrderBook, MarketDataRequest, decode_app_message book = FixOrderBook() -async with client.market_data(on_message=lambda raw: book.apply(decode_app_message(raw))) as md: + +async def on_message(raw) -> None: + book.apply(decode_app_message(raw)) + +async with client.market_data(on_message=on_message) as md: await md.send(MarketDataRequest.subscribe(["KXNBAGAME-26MAY25NYKCLE-NYK"])) ... view = book.get("KXNBAGAME-26MAY25NYKCLE-NYK") # MarketDataBook | None (bids/offers) @@ -146,14 +162,20 @@ opted in via `FixConfig.receive_settlement_reports=True`. Large batches paginate across fragments correlated by `Symbol`; `SettlementReassembler` accumulates them into one report: +`SettlementReassembler.add()` expects a `MarketSettlementReport`, so guard the +decode (other message types, and an unrecognized frame, decode to a different +model or `None`): + ```python -from kalshi.fix import SettlementReassembler, decode_app_message +from kalshi.fix import MarketSettlementReport, SettlementReassembler, decode_app_message reasm = SettlementReassembler() -async def on_message(raw): - complete = reasm.add(decode_app_message(raw)) # MarketSettlementReport | None until whole - if complete is not None: - ... # complete.parties has every party for the batch +async def on_message(raw) -> None: + msg = decode_app_message(raw) + if isinstance(msg, MarketSettlementReport): + complete = reasm.add(msg) # MarketSettlementReport | None until the batch is whole + if complete is not None: + ... # complete.parties has every party for the batch async with client.post_trade(on_message=on_message) as session: ... diff --git a/docs/index.md b/docs/index.md index 796914cd..b44a5c5c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,10 +19,11 @@ markets API. channels), and a `KlearClient` for the Self-Clearing-Member settlement API (10 operations, cookie-session + MFA auth). See [Perps](perps.md). - **FIX protocol** — a hand-rolled, async-first FIX engine (FIXT.1.1 / FIX50SP2) - for both products: order-entry, drop-copy, market-data, order-groups, post-trade - (prediction), and RFQ (prediction) sessions with typed message models, automatic - sequence recovery, and order-book / settlement reassembly. `from kalshi import - FixClient` (prediction) / `MarginFixClient` (margin). See [FIX](fix.md). + for both products: order-entry, drop-copy, market-data, post-trade (prediction), + and RFQ (prediction) sessions — plus order-group management over the order-entry + session — with typed message models, automatic sequence recovery, and order-book + / settlement reassembly. `from kalshi import FixClient` (prediction) / + `MarginFixClient` (margin). See [FIX](fix.md). - **Sync and async parity** — `KalshiClient` and `AsyncKalshiClient` share one transport implementation; method names, kwargs, return types, and error behavior are identical. The one exception is WebSocket access, which is async-only.