From 369efb9813caa1ce1fc382a22609281b0524ede5 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 17:28:13 -0500 Subject: [PATCH] =?UTF-8?q?release:=202.5.0=20=E2=80=94=20post-v2.4=20mult?= =?UTF-8?q?i-reviewer=20SDK=20audit=20closure=20(#273)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 34 issues across security, HTTP transport, WebSocket reliability, models/types, REST resources, performance, and docs/testing. Two breaking changes folded in: orders.create() requires count+action explicitly (#242); six REST + three WS model fields widened str/float -> Decimal (#258, #259). Migration in docs/migration.md v2.4 -> v2.5 section. Closes #273. --- CHANGELOG.md | 258 +++++++++++++++++++++++++++++++++++++++++++++ ROADMAP.md | 16 +++ docs/migration.md | 238 +++++++++++++++++++++++++++++++++++++++++ kalshi/__init__.py | 2 +- pyproject.toml | 2 +- 5 files changed, 514 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da8c581..a866efcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,264 @@ All notable changes to kalshi-sdk will be documented in this file. +## 2.5.0 — 2026-05-21 + +Post-v2.4 multi-reviewer audit closure (`#273`). 34 issues across security, +HTTP transport, WebSocket reliability, models/types, REST resources, +performance, and docs/testing — identified by a 7-agent parallel review on +top of the v2.4 sweep and executed across 4 sequential waves (W0 docs, W1 +money-risk, W2 medium, W3 polish) in disjoint git worktrees. 20 PRs merged; +main is mypy --strict clean, ruff clean, 2742 unit tests passing. + +### Breaking changes + +Two user-visible breakages, both fence-and-forget. Migration in +`docs/migration.md` v2.4 → v2.5 section. + +- **`orders.create()` kwarg path requires `count` and `action` explicitly** + (`#242`). Previously `client.orders.create(ticker=..., side="yes")` placed + a 1-contract live buy because `action` defaulted to `"buy"` and `count` to + `1`. Now raises `TypeError` before any HTTP request. The `request=...` + overload is unaffected; `CreateOrderRequest.count` no longer has a default. +- **Six REST + three WS model fields widened from `str`/`float` to `Decimal`** + (`#258`, `#259`). WS: `OrderGroupPayload.contracts_limit` (`str` → + `FixedPointCount`), `TickerPayload.dollar_volume` and `dollar_open_interest` + (`str` → `DollarDecimal`). REST: `Market.floor_strike`, `Market.cap_strike`, + `Event.fee_multiplier_override`, `MarketLifecyclePayload.floor_strike`, + `Series.fee_multiplier`, `SeriesFeeChange.fee_multiplier` (bare `Decimal` + or `float` → `DollarDecimal` / `Decimal` via `_coerce_decimal`). Wire + format unchanged; consumers must adopt `Decimal` arithmetic. + +### Critical (money-risk fixes) + +- **WS: validation failure on a sequenced frame no longer silently advances + the seq watermark** (`#241`). Before: a malformed `orderbook_delta` / + `order_group_update` frame was logged + skipped but seq tracking had + already advanced, so the next legitimate frame matched expected-seq and + gap detection never fired — local orderbook silently corrupted with no + resync trigger. After: pre-validate + apply + dispatch is wrapped in a + try/except that rolls back the watermark on any exception, so the next + delta triggers a real gap-recovery resubscribe. +- **REST/WS split-environment combinations rejected at construction** + (`#239`). Before: `KalshiClient(demo=True, + base_url="https://api.elections.kalshi.com/...")` (or the env-var + equivalent) silently produced a config where REST hit production but WS + hit demo — a WS-driven strategy could trade real money against a demo + book. After: `KalshiConfig.__post_init__` rejects mismatched REST/WS + hosts and the constructors raise `ValueError` with both inputs named. +- **`CreateOrderRequest.buy_max_cost` validator rejects `bool`** (`#243`). + Before: `buy_max_cost=True` slipped through as `1` (1¢ cap) because + `bool` is an `int` subclass. After: explicit rejection, matching the + `_coerce_decimal` invariant set by v2.4's `#225`. +- **`orders.create()` no longer silently defaults to 1-contract buy** + (`#242`, see Breaking changes). +- **Transport retries network-level httpx errors on idempotent verbs** + (`#240`). `ConnectError` / `NetworkError` / `RemoteProtocolError` / + `ReadError` / `WriteError` now participate in the same `RETRYABLE_METHODS` + + backoff + total-timeout loop the timeout branch already uses for + GET/HEAD/OPTIONS. `ConnectError` on POST/DELETE is also safe (request + never reached the wire, mirroring v2.4's `#204` PoolTimeout carve-out). + Other transport errors on non-idempotent verbs still surface immediately + so the caller can reconcile via `client_order_id`. New + `KalshiNetworkError` raised when retries are exhausted. + +### High-impact correctness + +- **Errors `408` and `504` route to `KalshiTimeoutError`** (`#251`). Carries + the "may or may not have committed" semantic from v2.4's `#226`; callers + can branch on `except KalshiTimeoutError` and reconcile. +- **Suppressed error bodies preserve the typed exception class** (`#252`). + A 429/401/409/422/504 whose body exceeds the 16KB `Content-Length` cap + still routes to the right subclass instead of degrading to + `KalshiError`. 429 Retry-After is still populated. +- **WS: resubscribe-window stash drained on every gap recovery** (`#254`). + Frames captured between unsubscribe-ack and the new subscribe-ack are + replayed through `_process_frame` for the new sid (filtered by sid; + others dropped with a debug log). Was previously only drained on full + reconnect, so per-gap stashes accumulated until the next disconnect. +- **WS: stale orderbook frames no longer mutate the local book** (`#255`). + Snapshot/delta apply is gated on subscription-existence at the configured + sid; frames arriving after teardown short-circuit before validation, + fixing a race where the high-level `subscribe_book` iterator could read a + stale book. +- **WS: `_OrderbookIterator` raises `KalshiOrderbookUnavailableError` + instead of yielding an empty `Orderbook`** (`#257`). Before: if + `mgr.get(ticker)` returned `None` mid-resync, the iterator yielded an + empty book — indistinguishable from a real zero-liquidity market. After: + typed error surfaces the race so the caller can reattach to a fresh + iterator after the resync snapshot lands. +- **WS: `KalshiBackpressureError` carries structured `channel` / `sid` / + `client_id` / `maxsize`** (`#256`). Matches the structured-error contract + v2.4's `#213` established for `KalshiSequenceGapError` / + `KalshiSubscriptionError`. +- **WS: orphan `subscribed` acks released** (`#268`). If a `subscribe` task + is cancelled between send and ack, the server still completes the + subscription; the dispatcher now detects the orphan ack (sid present, no + client_id mapping) and emits `unsubscribe` so the sid doesn't leak. +- **WS: snapshot payload `yes`/`no` required** (`#268`). A malformed + snapshot now raises `ValidationError` (which pairs cleanly with `#241`'s + seq rollback) instead of silently materializing an empty book. +- **Auth: conflicting key inputs rejected explicitly** (`#249`). + `KalshiClient(private_key_path=..., private_key=...)` and dual env-var + (`KALSHI_PRIVATE_KEY` + `KALSHI_PRIVATE_KEY_PATH`) now raise instead of + silently preferring one source. +- **Unknown `base_url` host fails closed by default** (`#250`). New + `KalshiConfig.allow_unknown_host=False` rejects hosts outside + `{api.elections.kalshi.com, demo-api.kalshi.co, localhost, 127.0.0.1, + ::1}` so a typo like `kalsi.com` no longer delivers signed requests to + an attacker. Opt-in via the field or `KALSHI_ALLOW_UNKNOWN_HOST=1`. +- **`Decimal('NaN')` / `Infinity` rejected at the boundary** (`#270`). + `_coerce_decimal` calls `is_finite()` after coercion so a downstream + arithmetic NaN never ships `"NaN"` to a real-money order endpoint. +- **Sign-executor close race fixed** (`#267`). `_get_sign_executor` + rechecks `_closed` under the lock so a racing `close()` can't leave a + freshly-instantiated `ThreadPoolExecutor` dangling. + +### Performance + +- **WS recv loop drops per-frame `asyncio.Task` + `asyncio.shield`** + (`#245`). Cooperative pause via `Event` + 50 ms poll instead of + allocating a `Task` / `Future` / contextvars copy per frame. Inline + dispatch on the hot path. +- **`subscribe_book` iterator caches the materialized `Orderbook`** + (`#244`). Memoized on `_BookState`, invalidated by the in-place apply + helpers; eliminates the O(n log n) sort + 2N `OrderbookLevel` + validations per delta that the high-level iterator was paying on top of + v2.4's `#199` in-place fast path. +- **WS snapshot apply collapses to a single dict walk** (`#263`). + `OrderbookSnapshotPayload.yes` / `.no` validate directly into + `dict[Decimal, Decimal]` via a `BeforeValidator`; `_apply_snapshot_inplace` + adopts the dict in identity (no rebuild). ~5× faster on a 200-level book. +- **`Page.to_dataframe` / `to_polars` built column-oriented** (`#264`). + Replaces per-row `model_dump(mode="python")` with a single getattr-driven + column build. Preserves the v2.4 Decimal contract; nested-model cells + still dumped per-column so polars Struct inference works. +- **REST has a pluggable JSON loader** (`#260`). New + `KalshiConfig.rest_json_loads` mirrors the existing `ws_json_loads`; set + to `orjson.loads` for ~2–3× faster list-endpoint parsing. +- **Signing path skips regex on the common case** (`#261`). + `_normalize_percent_encoding` short-circuits when no `%` appears. +- **Header merge hoisted out of the retry loop** (`#262`). Only + `auth_headers` changes per attempt; the 3-way `config_extra + + per_call_extra + body_headers` merge is now precomputed once per + request, saving N-1 dict copies across retries. +- **`MessageQueue._size` counter dropped** (`#271`). `qsize()` derives from + `len(self._buffer)` adjusted for the sentinel; two ints saved per put/get + on the WS hot path. +- **`import asyncio` hoisted out of `AsyncTransport.request`** (`#271`). + Stray per-request `sys.modules` lookup eliminated. + +### Configuration knobs (additive) + +- `KalshiConfig.rest_json_loads` (`#260`). +- `KalshiConfig.allow_unknown_host` + `KALSHI_ALLOW_UNKNOWN_HOST` env var + (`#250`). +- `extra_headers=` plumbed through every public REST resource method — + 302 method signatures via codemod (`#253`). `KALSHI-ACCESS-*` signing + headers always win, so callers cannot forge them via this surface. + +### Typed-exception expansion + +- New `KalshiNetworkError` (`#240`) — exhausted retries on a network-level + httpx error. +- New `KalshiOrderbookUnavailableError` (`#257`) — `_OrderbookIterator` + race where `mgr.get(ticker)` returned `None` mid-resync. +- `KalshiBackpressureError` gains `channel` / `sid` / `client_id` / + `maxsize` keyword-only fields (`#256`). + +### Models, types, request shape + +- **WS `user_orders` + `communications` payloads use `pydantic.AwareDatetime`** + (`#270`). Closes the gap left by v2.4's `#234` REST sweep — naive RFC3339 + strings now raise `ValidationError` on WS too. +- **V1 `CreateOrderRequest` enum-style fields narrowed to `Literal[...]`** + (`#270`). Closes the V1/V2 strictness gap; users constructing the + request directly fail at construction instead of server-side. +- **`MultiplierDecimal` alias + `_coerce_decimal` on multiplier fields** + (`#259`, see Breaking changes). +- **Internal `UnixSecondsTimestamp` alias** (`#270`). Documents the + seconds-vs-milliseconds wire-shape choice on `Balance.updated_ts`, + `Deposit.created_ts` / `finalized_ts`, `Withdrawal.created_ts` / + `finalized_ts`. +- **`Retry-After` past-date / negative form clamps to 0.0** (`#267`). Both + delta-seconds and HTTP-date branches now agree (was: negative delta- + seconds fell back to computed backoff while past HTTP-date retried + immediately). + +### REST resources + +- **`portfolio.positions_all()` / `fcm.positions_all()`** (`#269`). Both + endpoints are cursor-paginated; previously they shipped no `*_all()` + iterator, breaking the SDK's pagination convention. +- **`multivariate.lookup_history` validates `lookback_seconds` enum + locally** (`#269`). Spec restricts to `{10, 60, 300, 3600}`; passing + anything else now raises `ValueError` before the round trip. +- **Three deprecated multivariate endpoints marked + `@typing_extensions.deprecated`** (`#269`). `lookup_tickers`, + `lookup_history`, `create_market` (sync + async) carry the spec's + "should not be used for new integrations" message; emit + `DeprecationWarning` on first call. +- **`event_ticker` accepts `list[str] | str`** on `OrdersResource.list` / + `list_all` (`#269`). Joined via `_join_tickers(values, max_items=10)` + matching the spec's `MultipleEventTickerQuery`; the kwarg previously + typed `str` only. + +### `from_env` ergonomics + +- **`KalshiClient.from_env(**kwargs: Unpack[ClientInitKwargs])`** (`#266`). + `from_env` now exposes a `typing.Unpack`-driven TypedDict so typos like + `time_out=10` trip mypy strict at the call site; the internal + `# type: ignore[arg-type]` is gone. + +### Documentation + +- `docs/migration.md` gains a `v2.4 → v2.5` section covering the two + breaking changes above. +- `docs/migration.md` v2.3 → v2.4 section added (`#246`) covering the V1 + batch shape change, the new typed exceptions, passphrase-protected PEMs, + HTTP/2, and per-request `extra_headers`. +- `docs/resources/orders.md` batch_create / batch_cancel examples + rewritten for the v2.4 typed-response shape (`#247`). +- README pagination quickstart uses `Page.has_next` (was `has_more`, which + never existed) (`#248`). +- Three docstrings retagged from `v3.0.0 BREAKING` to `v2.4.0` where the + change actually shipped (`#265`). +- `pyproject.toml` gains `Framework :: AsyncIO`, `Operating System :: OS + Independent`, and `Topic :: Office/Business :: Financial :: Investment` + classifiers for PyPI discoverability (`#272`). +- Stale `Unreleased (post-v2.2.0)` bullet removed from ROADMAP (`#272`). + +### Testing + +- **34 regression tests added** across waves W1 / W2 / W3 (TDD per issue). +- **`tests/test_split_env.py`** — 12 tests covering the REST/WS host + cross-check (`#239`). +- **`tests/test_http2.py`** (skipif `h2` missing) — asserts `http2=True` + propagates to the underlying httpx pools on both sync + async clients + (`#271`). +- **`tests/test_rest_json_loader.py`** — six tests covering the new + `rest_json_loads` hook (`#260`). +- **`tests/test_extra_headers_plumbing.py`** — 245 reflective tests + asserting every public resource method exposes the new `extra_headers` + kwarg (`#253`). +- **Two new bench scripts**: `scripts/bench_orderbook_iterator.py` (drives + `_OrderbookIterator.__anext__` end-to-end) and + `scripts/bench_page_to_dataframe.py` (`#271`). +- **Hermetic test fixtures**: `tests/conftest.py` strips `KALSHI_*` env at + import + enables `KALSHI_ALLOW_UNKNOWN_HOST=1` process-wide so existing + tests using `https://test.kalshi.com` still work alongside the new + default-fail. + +### Breaking changes summary + +1. **`#242`** — `orders.create()` requires `count` and `action` + explicitly on the kwarg path (no silent 1-contract buy). +2. **`#258` + `#259`** — six REST model fields + three WS payload fields + widened from `str` / `float` to `Decimal`. Wire format unchanged; + consumers must adopt `Decimal` arithmetic. + +See `docs/migration.md` v2.4 → v2.5 for code-level migration snippets. + ## 2.4.0 — 2026-05-21 Comprehensive multi-reviewer audit (`#224`) — 33 issues across security, HTTP diff --git a/ROADMAP.md b/ROADMAP.md index 63253ad7..628ffef1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,6 +4,22 @@ See `CHANGELOG.md` for full release history. +- **v2.5.0 (2026-05-21)** — post-v2.4 multi-reviewer SDK audit closure + (#273, 34 issues across 7 surfaces). Critical fixes: WS seq watermark + rolls back on validation failure (#241), REST/WS split-environment + rejected (#239), `buy_max_cost` rejects bool (#243), network-level + httpx errors retry on idempotent verbs (#240, `KalshiNetworkError`). + Two breaking changes folded in: `orders.create()` requires + `count`+`action` (#242), six REST + three WS model fields widened + `str`/`float` → `Decimal` (#258, #259). Performance wins on the WS + hot path: per-frame Task/shield dropped (#245), materialized + Orderbook cached on `_BookState` (#244), snapshot apply single dict + walk (#263), `Page.to_dataframe` column-oriented (#264), pluggable + REST JSON loader (#260). DX: `extra_headers=` plumbed through every + public resource method (#253), `KalshiConfig.allow_unknown_host` + default-fail (#250). Executed across 4 sequential waves (W0 docs, + W1 money-risk, W2 medium, W3 polish) — 20 PRs merged. + - **v2.4.0 (2026-05-21)** — multi-reviewer SDK audit closure (#224, 33 issues across 7 surfaces). Critical fixes: WS orderbook resync on sequence gap (#189), DataFrame Decimal preservation (#190), positional diff --git a/docs/migration.md b/docs/migration.md index 6b24c0a1..7a1f378c 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,5 +1,243 @@ # Migration +## v2.4 → v2.5 + +v2.5 ships two user-visible breaking changes — both surface bugs that +v2.4's audit missed — plus performance wins on the WS hot path, REST +per-request headers, and a pluggable REST JSON loader. Wire format is +unchanged. + +### Breaking: `orders.create()` requires `count` and `action` + +Per #242, the kwarg-form `client.orders.create(ticker=..., side=...)` +previously defaulted `action` to `"buy"` and `count` to `1` if you +forgot them — placing a 1-contract live buy with no error. Now both are +required on the kwarg path and the model: `CreateOrderRequest.count` +no longer has a default. + +```python +# v2.4 (silent 1-contract buy on the missing kwargs) +client.orders.create(ticker="EXAMPLE-25-T", side="yes") + +# v2.5 (raises TypeError before any HTTP request) +client.orders.create(ticker="EXAMPLE-25-T", side="yes") +# TypeError: create() requires `ticker`, `side`, `count`, and `action` + +# v2.5 (explicit, works) +client.orders.create( + ticker="EXAMPLE-25-T", + side="yes", + action="buy", + count=10, + yes_price="0.65", +) +``` + +The `request=...` overload is unaffected — `CreateOrderRequest(...)` is +the recommended path for programmatic order construction. + +### Breaking: WS + REST model fields widened to `Decimal` + +Per #258 and #259, nine model fields previously typed as `str` or +`float` are now `Decimal`-backed via `DollarDecimal` / +`FixedPointCount` / `Decimal`-via-`_coerce_decimal`. This brings them +under the same coercion contract every other money/count field in the +SDK has used since v2.0: + +| Field | Was | Now | +|---|---|---| +| `OrderGroupPayload.contracts_limit` (WS) | `str \| None` | `FixedPointCount \| None` | +| `TickerPayload.dollar_volume` (WS) | `str` | `DollarDecimal` | +| `TickerPayload.dollar_open_interest` (WS) | `str` | `DollarDecimal` | +| `Market.floor_strike` | `Decimal \| None` (bare) | `DollarDecimal \| None` | +| `Market.cap_strike` | `Decimal \| None` (bare) | `DollarDecimal \| None` | +| `Event.fee_multiplier_override` | `Decimal \| None` (bare) | `Decimal \| None` (coerced) | +| `MarketLifecyclePayload.floor_strike` (WS) | `Decimal \| None` (bare) | `DollarDecimal \| None` | +| `Series.fee_multiplier` | `float` | `Decimal` (coerced) | +| `SeriesFeeChange.fee_multiplier` | `float` | `Decimal` (coerced) | + +Wire format is unchanged — the spec already specified these as decimal +strings or fixed-point. The behavior change is at the Python boundary: + +```python +# v2.4 +ticker_payload.dollar_volume # "1234.5600" (str) +ticker_payload.dollar_volume + 1.0 # TypeError + +# v2.5 +ticker_payload.dollar_volume # Decimal("1234.5600") +ticker_payload.dollar_volume + Decimal("1.00") # Decimal arithmetic +``` + +If you were already wrapping these in `Decimal(...)` at the consumer +side, that wrapper now becomes a no-op identity coercion — safe to +keep or remove. Float arithmetic against `Series.fee_multiplier` will +now raise `TypeError`; coerce to `Decimal` first. + +### Additive: per-request `extra_headers` + +Per #253, every public REST resource method now accepts an +`extra_headers` kwarg for distributed-tracing, idempotency, or +per-call routing. `KALSHI-ACCESS-*` signing headers always win, so +callers cannot forge them via this surface. + +```python +import uuid + +client.orders.create( + ticker="EXAMPLE-25-T", side="yes", action="buy", + count=10, yes_price="0.65", + extra_headers={"Idempotency-Key": str(uuid.uuid4())}, +) +``` + +Client-wide defaults still go via `KalshiConfig.extra_headers`; per-call +values merge on top (later wins). + +### Additive: pluggable REST JSON loader + +Per #260, `KalshiConfig.rest_json_loads` mirrors the existing +`ws_json_loads` (v2.4 `#209`). Set to `orjson.loads` for ~2–3× faster +list-endpoint parsing: + +```python +import orjson +from kalshi import KalshiClient, KalshiConfig + +config = KalshiConfig(rest_json_loads=orjson.loads) +client = KalshiClient(auth=..., config=config) +``` + +Default (`None`) falls back to `httpx.Response.json()`. + +### Additive: unknown-host default-fail + +Per #250, `KalshiConfig` now rejects `base_url` / `ws_base_url` hosts +outside `{api.elections.kalshi.com, demo-api.kalshi.co, localhost, +127.0.0.1, ::1}` by default. A typo like `kalsi.com` no longer silently +delivers signed requests to the wrong endpoint. Opt-in for mock servers +or custom proxies: + +```python +# Either: +config = KalshiConfig( + base_url="https://my-mock-server.test/trade-api/v2", + allow_unknown_host=True, +) + +# Or process-wide: +os.environ["KALSHI_ALLOW_UNKNOWN_HOST"] = "1" +``` + +### Additive: split REST/WS environment guard + +Per #239, `KalshiClient(demo=True, base_url="https://api.elections.kalshi.com/...")` +(or the env-var equivalent) now raises `ValueError` at construction +instead of silently producing a config where REST hits production but +WS hits demo. If you genuinely need mixed environments, build the +`KalshiConfig` explicitly with both `base_url` and `ws_base_url` +pointing at hosts in the same environment. + +### Additive: new typed exceptions + +- `KalshiNetworkError` (#240) — exhausted retries on `httpx.ConnectError`, + `NetworkError`, `RemoteProtocolError`, `ReadError`, or `WriteError`. + Transport now retries these on idempotent verbs (GET/HEAD/OPTIONS) and + on `ConnectError` for POST/DELETE (request never reached the wire). +- `KalshiOrderbookUnavailableError` (#257) — raised by the high-level + `subscribe_book` iterator when the local book is missing between a + gap-recovery teardown and the new snapshot. Catch and reattach to a + fresh iterator: + + ```python + while True: + try: + async for book in session.subscribe_book(ticker="EXAMPLE-25-T"): + ... + except KalshiOrderbookUnavailableError: + # gap recovery in flight; reattach + continue + ``` + +- `KalshiBackpressureError` now carries `channel`, `sid`, `client_id`, + and `maxsize` (#256). Consumers iterating multiple subscriptions can + route the error by channel: + + ```python + try: + async for msg in session.subscribe_ticker(tickers=[...]): + ... + except KalshiBackpressureError as e: + logger.warning("backpressure on %s (sid=%s)", e.channel, e.sid) + ``` + +### Additive: `from_env(**kwargs: Unpack[ClientInitKwargs])` + +Per #266, `KalshiClient.from_env` and `AsyncKalshiClient.from_env` now +expose a `typing.Unpack`-driven TypedDict signature so typos like +`time_out=10` trip mypy strict at the call site. No runtime change. + +### Additive: `portfolio.positions_all()` / `fcm.positions_all()` + +Per #269, both endpoints now ship cursor-iterating `*_all()` helpers +matching the rest of the SDK's pagination convention. + +```python +for position in client.portfolio.positions_all(): + print(position.ticker, position.position) +``` + +### Multivariate endpoints emit `DeprecationWarning` + +Per #269, `multivariate.lookup_tickers`, `multivariate.lookup_history`, +and `multivariate.create_market` (sync + async) carry +`@typing_extensions.deprecated` decorators citing the spec's "should +not be used for new integrations" guidance. Use RFQs instead. The +endpoints still work; calls just emit a `DeprecationWarning` on first +use. + +`multivariate.lookup_history` also now validates `lookback_seconds` +locally against the spec enum `{10, 60, 300, 3600}` and raises +`ValueError` for any other value before the round trip. + +### `orders.list(event_ticker=...)` accepts lists + +Per #269, `event_ticker` accepts `list[str] | str | None` on +`OrdersResource.list` and `list_all` (sync + async). Lists are joined +via `_join_tickers` with a spec-enforced `max_items=10`: + +```python +for order in client.orders.list_all(event_ticker=["EV1", "EV2", "EV3"]): + ... +``` + +### Observable but not breaking + +- WS recv loop no longer allocates a `Task` + `shield` per frame (#245). + Pause/resume now uses a cooperative `Event` + 50 ms poll; lower + allocation pressure on high-volume markets. +- `subscribe_book` iterator caches the materialized `Orderbook` per + ticker (#244); consecutive `mgr.get(ticker)` calls without intervening + mutations return the same object identity. +- `OrderbookSnapshotPayload.yes` and `.no` are now `dict[Decimal, Decimal]` + rather than `list[tuple[Decimal, Decimal]]` (#263). External consumers + reading these fields directly will see a dict; iterate `.items()` if + you need the prior tuple shape. +- `OrderbookSnapshotPayload.yes`/`.no` are now required (no `default=[]`); + a malformed snapshot raises `ValidationError` (#268). +- `Decimal('NaN')` / `Decimal('Infinity')` rejected at parse and at + serialize (#270); construct fresh values from finite inputs. +- WS `user_orders` and `communications` payloads now use + `pydantic.AwareDatetime` (#270); naive RFC3339 strings raise + `ValidationError` (REST was already strict per v2.4 #234). +- V1 `CreateOrderRequest` enum-style fields (`side`, `action`, + `time_in_force`, `self_trade_prevention_type`) are now + `Literal[...]` typed (#270); typos fail at construction instead of + server-side. +- `Retry-After: -5` and `Retry-After: ` now both clamp + to 0.0 (retry immediately) — the delta-seconds form previously fell + back to computed backoff while the date form already clamped (#267). + ## v2.3 → v2.4 v2.4 ships one user-visible breaking change — the V1 batch order diff --git a/kalshi/__init__.py b/kalshi/__init__.py index dc318996..371627a9 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -336,4 +336,4 @@ "Withdrawal", ] -__version__ = "2.4.0" +__version__ = "2.5.0" diff --git a/pyproject.toml b/pyproject.toml index 28bda234..ab12c29b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "2.4.0" +version = "2.5.0" description = "A professional Python SDK for the Kalshi prediction markets API" readme = "README.md" license = { text = "MIT" }