Skip to content

Commit 47017e4

Browse files
TexasCodingaudit-w0c
andauthored
docs: add v2.1→v2.3 migration sections; fix list_all cap claim; align WS channel docs (#227)
- migration.md: new v2.1→v2.2 and v2.2→v2.3 sections; correct list_all() page-cap contradiction (unbounded by default since v2.0). - README + docs/websockets.md: agree on 11 typed subscribe_* methods (+ generic escape hatch where applicable). Use SDK method suffixes (subscribe_market_lifecycle, subscribe_multivariate_lifecycle, subscribe_order_group) instead of wire-channel names. Closes #200, Closes #218 Refs #224 Co-authored-by: audit-w0c <audit@local>
1 parent f64c07f commit 47017e4

2 files changed

Lines changed: 165 additions & 10 deletions

File tree

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi
1111
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
1212
[![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/)
1313

14-
- **Full coverage** of the Kalshi REST API (98 operations across 19 resources, OpenAPI v3.18.0) and WebSocket API (13 channels).
14+
- **Full coverage** of the Kalshi REST API (98 operations across 19 resources, OpenAPI v3.18.0) and WebSocket API (11 typed `subscribe_*` channels + 2 escape-hatch).
1515
- **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.
1616
- **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`.
1717
- **Sync and async** clients sharing one transport — no thread-pool wrapping.
@@ -191,12 +191,16 @@ async def main() -> None:
191191
asyncio.run(main())
192192
```
193193

194-
Available channels (13): 11 have dedicated `subscribe_*` methods — `ticker`,
195-
`trade`, `orderbook_delta`, `fill`, `market_positions`, `user_orders`,
196-
`order_group_updates`, `market_lifecycle_v2`, `multivariate`,
197-
`multivariate_market_lifecycle`, `communications`. The remaining two
198-
(`control_frames`, `root`) are reachable through the generic
199-
`subscribe(channel, ...)` escape hatch.
194+
Available channels (11 typed + 2 escape-hatch). Eleven have dedicated
195+
`subscribe_*` methods — `subscribe_ticker`, `subscribe_trade`,
196+
`subscribe_orderbook_delta`, `subscribe_fill`, `subscribe_market_positions`,
197+
`subscribe_user_orders`, `subscribe_order_group`,
198+
`subscribe_market_lifecycle`, `subscribe_multivariate`,
199+
`subscribe_multivariate_lifecycle`, `subscribe_communications`. The
200+
AsyncAPI-declared `control_frames` and `root` channels are reachable
201+
through the generic `subscribe(channel, ...)` escape hatch. See
202+
[docs/websockets.md](docs/websockets.md#the-11-channels) for the full
203+
channel table.
200204

201205
## Error handling
202206

docs/migration.md

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,154 @@
11
# Migration
22

3+
## v2.2 → v2.3
4+
5+
v2.3 is additive on the REST surface and **soft-breaking on the WebSocket
6+
surface** in one place: `KalshiWebSocket.run_forever()` no longer returns
7+
silently when nothing has been subscribed. Everything else is opt-in.
8+
9+
### `run_forever()` requires an active subscription
10+
11+
Per closed #175 / #185, `KalshiWebSocket.run_forever()` now raises
12+
`KalshiSubscriptionError` at the call site when no `subscribe_*` call has
13+
landed on the session. The previous silent no-op masked a common mistake —
14+
registering an `@ws.on(channel)` callback without subscribing, then
15+
wondering why no frames arrived (the server only sends frames for channels
16+
the client explicitly subscribed to).
17+
18+
```python
19+
# v2.2 — silently returned, recv loop never ran
20+
async with ws.connect() as session:
21+
await session.run_forever()
22+
23+
# v2.3 — wrap a subscribe_* call before run_forever()
24+
async with ws.connect() as session:
25+
await session.subscribe_ticker(tickers=["EXAMPLE-25-T"])
26+
await session.run_forever()
27+
```
28+
29+
No production usage of the silent shape is known; the foot-gun was the
30+
bug.
31+
32+
### Cooperative shutdown via `run_forever(stop_event=...)`
33+
34+
Per closed #177 / #186, `run_forever()` now accepts an optional
35+
`stop_event: asyncio.Event | None = None`. When set, the recv loop
36+
closes the connection and exits cleanly without raising `CancelledError`
37+
— typically wired to `SIGINT` so Ctrl+C drains in-flight dispatches:
38+
39+
```python
40+
import asyncio
41+
import signal
42+
43+
stop = asyncio.Event()
44+
asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop.set)
45+
46+
async with ws.connect() as session:
47+
await session.subscribe_ticker(tickers=["EXAMPLE-25-T"])
48+
await session.run_forever(stop_event=stop)
49+
```
50+
51+
Omitting `stop_event` preserves v2.2 behavior — external cancellation
52+
still propagates.
53+
54+
### WS resubscribe-window frame stashing
55+
56+
Per #176, the reconnect path used to silently drop data frames that
57+
arrived between the moment `SubscriptionManager` cleared its
58+
`sid → client_id` map and the moment the new sids landed in the
59+
subscribe-response handler. Under burst reconnects on high-volume
60+
channels (`ticker`, `trade`, `fill`), this lost tens of messages per
61+
reconnect.
62+
63+
`SubscriptionManager` now stashes those frames in a per-sid bounded
64+
`collections.deque(maxlen=stash_maxlen)` (default 1000 per sid). After
65+
`resubscribe_all` completes, the stash is drained through the normal
66+
dispatch path so seq trackers advance, orderbook state applies, and
67+
iterator consumers receive the frames in arrival order. No API change —
68+
behavior change only.
69+
70+
### Async RSA-PSS sign offload — `KalshiAuth.close()` for standalone users
71+
72+
Per #178, `KalshiAuth.sign_request_async()` now routes the RSA-PSS sign
73+
through a dedicated `ThreadPoolExecutor(max_workers=2)` lazy-initialised
74+
on first use, so signs don't queue behind `getaddrinfo` / file I/O / other
75+
`to_thread()` work during WS reconnect storms.
76+
77+
If you use `KalshiAuth` **through** `KalshiClient` / `AsyncKalshiClient`,
78+
`client.close()` already tears the executor down for you and no migration
79+
is required. If you instantiate `KalshiAuth` **standalone** (e.g. signing
80+
requests through a custom transport), call `auth.close()` to release the
81+
executor:
82+
83+
```python
84+
from kalshi import KalshiAuth
85+
86+
auth = KalshiAuth.from_key_path("your-key-id", "~/.kalshi/private_key.pem")
87+
try:
88+
headers = await auth.sign_request_async("GET", "/trade-api/v2/markets")
89+
...
90+
finally:
91+
auth.close()
92+
```
93+
94+
---
95+
96+
## v2.1 → v2.2
97+
98+
v2.2 is **soft-breaking at the response-parse boundary only** (per
99+
#157 / #172). The wire format is unchanged. The behavior change: server
100+
omission of a previously-optional spec-required field now raises
101+
`pydantic.ValidationError` at parse time, instead of silently producing
102+
`field=None` and pushing the surprise to a downstream `NoneType`
103+
attribute access.
104+
105+
### What changed
106+
107+
226 fields across REST models, WS payloads, and helper classes flipped
108+
from `Optional[X] = None` to required, matching what the OpenAPI /
109+
AsyncAPI specs already declared.
110+
111+
Affected response model categories include `Market`, `Order`, `Fill`,
112+
`Event`, `EventMetadata`, `Settlement`, `Trade`, `IncentiveProgram`,
113+
`RFQ`, `Quote`, `OrderGroup` plus its three group-response shapes, and
114+
11 WebSocket payloads — including the new `*_ts_ms` millisecond
115+
timestamps and the `outcome_side` / `book_side` direction encoding the
116+
server already emits. See `CHANGELOG.md` for the full per-model
117+
breakdown.
118+
119+
### Recovery
120+
121+
If you parse live responses and previously branched on `field is None`
122+
for an optional you read from the SDK, you may now hit
123+
`pydantic.ValidationError` at the parse site when the server omits the
124+
field on a malformed event. Wrap the call site:
125+
126+
```python
127+
from pydantic import ValidationError
128+
129+
try:
130+
market = client.markets.get(ticker)
131+
except ValidationError as exc:
132+
logger.warning("skipping malformed market %s: %s", ticker, exc)
133+
continue
134+
```
135+
136+
The vast majority of fields were already populated in practice — only
137+
models with legitimate live-server omission gaps need the try/except.
138+
Two known cases (`Event.product_metadata`, `EventMetadata.market_details`)
139+
ship in v2.3 with `server_omits_despite_required` exclusions baked in
140+
and need no caller action.
141+
142+
### `extra="allow"` everywhere
143+
144+
All WS envelope and helper classes (and the five REST helpers from the
145+
v2.0 sweep — `Page`, `Orderbook`, `OrderbookLevel`, `BidAskDistribution`,
146+
`PriceDistribution`) now uniformly use `model_config = ConfigDict(extra="allow")`.
147+
Additive server fields no longer raise; they land on
148+
`__pydantic_extra__` and round-trip through `model_dump()`.
149+
150+
---
151+
3152
## v2.0 → v2.1
4153

5154
v2.1 syncs the SDK to OpenAPI spec v3.18.0. It's **additive at the resource
@@ -402,9 +551,11 @@ async for market in async_client.markets.list_all(status="open"):
402551
...
403552
```
404553

405-
`list_all()` walks cursors until the server returns no more pages, with a
406-
1000-page safety cap and a cursor-repeat detector that raises `KalshiError`
407-
if the server hands back a duplicate cursor.
554+
`list_all()` is unbounded by default — it walks cursors until the server
555+
returns no more pages. Pass `max_pages=N` for an explicit cap; passing
556+
`max_pages=0` raises `ValueError`. The cursor-repeat detector still raises
557+
`KalshiError` if the server hands back a duplicate cursor. See
558+
[Pagination](pagination.md) for the canonical contract.
408559

409560
## Still missing?
410561

0 commit comments

Comments
 (0)