Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **kalshi-python-sdk** (6266 symbols, 13386 relationships, 257 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **kalshi-python-sdk** (8490 symbols, 17402 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
206 changes: 206 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,212 @@

All notable changes to kalshi-sdk will be documented in this file.

## 2.7.0 — 2026-05-22

Post-v2.6 independent multi-LLM reviewer audit closure. **47 issues filed
(`#311`–`#357`) by a 9-reviewer parallel pass** combining 7 internal specialist
agents (security/auth, HTTP transport, WebSocket, models/contracts, sync/async
parity, performance, resources/API) with **fresh-eyes external LLM reviews via
the Codex CLI (GPT-5) and Gemini CLI**. 44 issues closed in this release across
4 sequential waves (W0 docs, W1 HIGH integrity, W2 MEDIUM correctness, W3 LOW
polish/perf/deps); 17 PRs merged. 3 breaking-rename issues (`#348`, `#349`,
`#351`) deferred to **v3.0.0** milestone. Main is mypy `--strict` clean, ruff
clean, **2876 unit tests passing** (≈100 new regression tests added).

### Breaking changes

Five behavioral fences; all surface bugs that were silently wrong or invariants
the SDK now enforces. Per project policy (established by v2.0–v2.6),
bug-surfacing behavioral fences ship in minor releases; intentional public-API
removals or renames are reserved for major releases — that's why the three
breaking renames (`#348`, `#349`, `#351`) are deferred to v3.0.0.

- **`AmendOrderRequest.side` / `.action` narrowed to `Literal`** (`#312`).
Mirrors the v2.5 `#270` narrowing on `CreateOrderRequest`. Previously any
string passed validation and the server rejected with a 400; now invalid
values raise `pydantic.ValidationError` at construction.
- **`KalshiConfig.extra_headers` immutable post-construction** (`#313`). The
`#298` `KALSHI-ACCESS-*` fence ran once at construction; post-construction
mutation of `extra_headers` could reopen the auth-header forge surface.
`extra_headers` is now stored as `MappingProxyType` over a defensive copy
— `config.extra_headers["k"] = "v"` raises `TypeError`. Construction-time
fence unchanged.
- **`CommunicationsResource.list_rfqs` / `list_quotes` `status` kwarg
narrowed to `Literal`** (`#324`). New `RfqStatusLiteral` / `QuoteStatusLiteral`
match the spec's closed set; arbitrary `str` is rejected by mypy at the
call site. Consistent with the existing `OrdersResource.list(status=...)`
pattern.
- **`to_decimal()` rejects `NaN` / `Infinity`** (`#325`). The public helper
promised safety in its docstring but accepted any `Decimal`. Now raises
`ValueError` on non-finite inputs, matching the `_coerce_decimal` validator.
- **`DollarDecimal` request-side fields reject negative prices and
sub-tick precision** (`#343`). `CreateOrderRequest`,
`AmendOrderRequest`, `CreateOrderV2Request`, `AmendOrderV2Request` now
reject negative `yes_price` / `no_price` and sub-$0.0001-tick precision
at construction. Response-side `DollarDecimal` is unchanged (servers may
emit any value).

### Critical (HIGH-severity money/auth/data-integrity fixes)

- **`KalshiClient.from_env` preserves caller ownership of `KalshiAuth`**
(`#311`). The `from_env` classmethod (sync and async) overwrote `_auth_owned`
from the input kwarg instead of recomputing from what `__init__` actually
did. Two real bugs: (a) `from_env(key_id=..., private_key=...)` with no env
vars set leaked the sign `ThreadPoolExecutor` because the SDK-built auth
was flagged "not owned"; (b) `from_env(auth=my_auth)` with env vars set
caused `client.close()` to close the caller's still-referenced auth,
raising `RuntimeError("KalshiAuth has been closed")` on the next
`sign_request`. The fix recomputes ownership from `__init__`'s invariant.
- **WS `_wait_for_response` wraps `TimeoutError` as `KalshiSubscriptionError`**
(`#314`). `asyncio.wait_for` raises bare `TimeoutError` on timeout, which
escaped `subscribe()` / `unsubscribe()` unhandled because only
`ConnectionClosed` was caught. Now wrapped with `channel` / `client_id` /
`op` context so consumers branching on SDK exceptions actually see the
expected type.
- **WS zombie-subscription cleanup on gap-recovery failure** (`#315`).
`broadcast_error` never popped the dead `Subscription`. A failed
`resubscribe_one` in `_handle_seq_gap` (and the `KalshiBackpressureError`
path) left a zombie sub whose closed queue persisted; the next reconnect's
`resubscribe_all` resurrected it on the server — silent data loss + a
server-quota leak. `broadcast_error` now pops `_subscriptions` and
`_sid_to_client` so reconnects can't resurrect dead subs.

### High-impact correctness

- **`from_env` lazy `try_from_env()` evaluation** (`#316`). The classmethod
eagerly evaluated `KalshiAuth.try_from_env()` even when the caller passed
`auth=` / `key_id` / `private_key` / `private_key_path`. A malformed
`KALSHI_PRIVATE_KEY` in the process env then raised on every CI worker
that bypassed env. Now gated on `caller_supplied_auth`.
- **`sign_request` strips URL fragment** (`#317`). `path.split("?")[0]`
stripped query strings but not `#fragment`. httpx drops fragments before
sending, producing a guaranteed signature mismatch (401) on any path
containing `#`. Now strips both.
- **`Retry-After` honored across 408 / 429 / 503 / 504** (`#322`). Previously
only 429 parsed `Retry-After`. Per RFC 7231 §7.1.3 the header applies to
all four. `_parse_retry_after` extracted; `retry_after` lifted to
`KalshiError` base class so `KalshiServerError` and `KalshiTimeoutError`
also surface the hint.
- **`Retry-After` path applies jitter** (`#321`). Synchronized clients
hitting the same 429/503 window would stampede the rate limit at the
exact server-suggested moment. Full Jitter is now added on top of the
server floor, capped at `retry_max_delay`. Preserves RFC compliance
(`Retry-After` is a "no sooner than" hint).
- **Response body size cap on success path** (`#323`). The `#203` 16 KB
cap applied only to error responses; the success path was unbounded.
New `_enforce_response_body_cap` enforces the same protection on
non-streaming success bodies via a Content-Length pre-check plus a
post-buffer guard.
- **V1 order request models gain `ge=0` parity** (`#326`). `subaccount`,
`exchange_index`, `subaccount_number` on `CreateOrderRequest`,
`AmendOrderRequest`, `BatchCancelOrderRequest`, and related V1 + group
models now reject negative integers (matching V2 and the `#295` sweep).
- **`Page._columns` handles `None`-first nullable nested columns** (`#328`).
Detection inspected only `cols[0]`; a `None`-first nullable nested
`BaseModel` column skipped the `model_dump` pass and broke
`to_dataframe` / `to_polars` for nullable-Struct schemas.
- **WS `OrderbookDeltaPayload.ts` typed as `AwareDatetime`** (`#331`).
Missed by the v2.5 `#270` WS datetime sweep. Tightening across
`orderbook_delta`, `user_orders`, and `communications` payloads.
- **WS backpressure error closes connection cleanly** (`#332`).
`KalshiBackpressureError` in the recv loop previously broadcast sentinels
and broke, but left the WS open and `_running=True`. The next
`subscribe_*` restarted the recv loop on top of orphan server-side subs.
Now closes the connection and clears `_running` / manager refs so the
next session starts fresh.

### Performance

- **Orderbook materialization via `model_construct`** (`#327`).
`_BookState.to_orderbook` previously re-validated every price level through
Pydantic on each `apply_delta`. Data is SDK-canonical after the snapshot
validation; switched to `model_construct` to skip the redundant pass.
- **Zero-delta orderbook updates preserve cache** (`#347`). A no-op delta
(quantity unchanged) used to invalidate the memoized `Orderbook` view,
defeating the `#244` cache. Now skipped when the price-level dict is
unchanged.
- **Public `apply_snapshot` single-copy** (`#344`). The public path
allocated `yes` / `no` dicts twice — once via identity in
`_apply_snapshot_inplace`, then again via `dict(msg.msg.yes)` to copy
defensively. The bypass path (recv hot loop, `#296`) keeps identity
adoption; the public path now copies once.
- **V2 batch endpoints use bytes-fast-path** (`#329`). `batch_create_v2` /
`batch_cancel_v2` paid the dict-walk serializer twice; the v2.4 `#223`
fast-path was never extended. Now serialize via `model_dump_json` once
and send the bytes.
- **RSA-PSS / MGF1 / SHA256 config cached** (`#345`). Previously
allocated per signature. Hoisted to module-level constants — measurable
on the auth hot path.
- **`SequenceTracker.track` sync fast-path** (`#330`). New public
`track_sync` lets the recv hot loop skip the per-frame coroutine-object
allocation; the async wrapper remains for callers that need to await on
the gap path.
- **WS recv `asyncio.timeout` swap** (`#356`). Replaced `asyncio.wait_for`
in the recv hot path with `async with asyncio.timeout(...)` (Python 3.11+)
— fewer TimerHandle allocations per frame.
- **`method.upper()` hoisted out of retry loop** (`#342`). Computed once
per request instead of once per retry attempt.

### Polish

- **`AsyncTransport.close()` cancellation-safe** (`#333`). Sets `_closed`
after `aclose()` returns, not before — cancellation between the two no
longer leaks the httpx pool. Mirrors the v2.6 `#301` sync fix.
- **`_delete_with_body(params=...)` symmetry** (`#340`). The body-only
variant gains the `params` kwarg that `_delete_with_body_json` already
accepted.
- **Pagination cycle detector catches non-adjacent loops** (`#352`). The
prior detector only caught adjacent cursor repeats; an `A→B→A→B` loop
from a load-balanced pod-state drift now raises before exhausting
`max_pages`. Memory-bounded at 1024 seen cursors.
- **WS `_handle_orphan_subscribed` correlation** (`#354`). Server
`unsubscribed` acks for sids the client never owned no longer clobber
unrelated state.
- **WS `ConnectionManager.reconnect` exc_info on failure** (`#355`). Per-
attempt failures now log with `exc_info=True` at DEBUG so root cause
(auth / TLS / DNS) survives to max-retry.
- **WS `_stop()` teardown order** (`#357`). Closes the connection FIRST,
then broadcasts sentinels — late in-flight frames no longer land on
closed queues. `close()` wrapped in `try/finally` so sentinels always
fire even if `close()` raises.
- **`KalshiConfig.extra_headers` doubled-pipeline removed** (`#341`).
Previously attached to `httpx.Client(headers=...)` defaults AND merged
per-request via `_ci_merge`. Now `_ci_merge` is the single source of
truth; the `#298` precedence contract (config defaults < per-call
extras < signed auth) is structurally enforced.
- **`orders.create()` overload tightening** (`#350`). The `**kwargs`
overload statically required `action` and `count` since v2.5 `#242`;
the type signature is now updated to match runtime so mypy catches
omissions at the call site.
- **Documentation drift sweep** (`#318`, `#319`, `#320`, `#334`, `#336`,
`#337`, `#338`, `#339`). Three live-data endpoint paths and the
`batch()` return shape corrected; `positions_all()` "does not exist"
claim dropped (it shipped in v2.5 `#269`); `structured_targets.get()`
404 mapping corrected (raises `KalshiNotFoundError`, not `None`);
sync/async docstring drift normalized across `orders`, `markets`,
`portfolio`, `communications`; `CreateOrderV2Request` docstring
corrected (`DollarDecimal`, not the nonexistent `FixedPointDollars`);
`LiveDataResource.get_typed` clarified as the legacy URL form.
- **`KalshiAuth.from_pem` OpenSSH-format error** (`#335`). Detecting an
`-----BEGIN OPENSSH PRIVATE KEY-----` header now raises a targeted
`KalshiAuthError` with the exact `ssh-keygen -p -m PKCS8 -f <path>`
conversion command instead of the generic PEM parse failure.

### Additive

- **`kalshi.OrderPrice`** — public type alias for the request-side
bounded `DollarDecimal` used by order-request models (`#343`).
- **`kalshi.RfqStatusLiteral`, `kalshi.QuoteStatusLiteral`** — public
Literal aliases for communications status filtering (`#324`).
- **`SequenceTracker.track_sync`** — public sync entry point for the WS
recv hot path (`#330`).

### Dependencies

- **`pydantic>=2.4`** (`#346`). Bumped from `>=2.0` to ensure the
`StrictInt` / `_coerce_decimal` invariants the SDK relies on get the
2.4+ semantics. Pydantic 2.0–2.3 also shipped JSON-parser bugs.

## 2.6.0 — 2026-05-22

Post-v2.5 independent reviewer audit closure (`#273` follow-on). 7 issues
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Reference issues from PRs via `Closes #N` so the issue closes on merge.
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **kalshi-python-sdk** (6266 symbols, 13386 relationships, 257 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **kalshi-python-sdk** (8490 symbols, 17402 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
29 changes: 29 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@

See `CHANGELOG.md` for full release history.

- **v2.7.0 (2026-05-22)** — post-v2.6 independent multi-LLM reviewer audit
closure (47 issues `#311`–`#357`, 44 closed; `#348` / `#349` / `#351`
deferred to v3.0.0). Identified by a 9-reviewer parallel pass combining 7
internal specialist agents with fresh-eyes Codex CLI (GPT-5) + Gemini CLI
reviews. Five breaking fences folded in: `AmendOrderRequest.side`/`.action`
narrowed to `Literal` (`#312`); `KalshiConfig.extra_headers` immutable
post-construction (`#313`); communications `status` kwarg `Literal`
(`#324`); `to_decimal()` rejects NaN/Inf (`#325`); `DollarDecimal` request
fields reject negative/sub-tick (`#343`). Critical fixes: `from_env`
caller-ownership invariant (`#311`), WS `TimeoutError` wrap (`#314`), WS
zombie-subscription cleanup (`#315`). High-impact correctness: lazy
`try_from_env` (`#316`), `sign_request` strips URL fragment (`#317`),
`Retry-After` honored across 408/429/503/504 with jitter (`#321`, `#322`),
success-body size cap (`#323`), V1 `ge=0` parity (`#326`), `Page._columns`
None-first nested (`#328`), WS `OrderbookDeltaPayload.ts` AwareDatetime
(`#331`), WS backpressure WS teardown (`#332`). Performance: orderbook
`model_construct` (`#327`), zero-delta cache preservation (`#347`),
`apply_snapshot` single-copy (`#344`), V2 batch bytes fast-path (`#329`),
PSS config cache (`#345`), `track_sync` (`#330`), `asyncio.timeout` recv
(`#356`), `method.upper` hoist (`#342`). Polish: async close
cancellation-safe (`#333`), `_delete_with_body(params=)` (`#340`),
non-adjacent pagination cycle (`#352`), orphan unsubscribe correlation
(`#354`), reconnect exc_info (`#355`), `_stop()` teardown order (`#357`),
dropped `extra_headers` dual pipeline (`#341`), `orders.create()` overload
(`#350`), docs + docstring drift (`#318`–`#320`, `#334`–`#339`), OpenSSH
key error message (`#335`). Dependencies: `pydantic>=2.4` (`#346`).
Executed across 4 sequential waves (W0 docs, W1 HIGH, W2 MEDIUM, W3 LOW)
in disjoint git worktrees — 17 PRs merged.

- **v2.6.0 (2026-05-22)** — post-v2.5 independent reviewer audit closure
(`#273` follow-on, 7 issues `#295`–`#301`). Two breaking changes folded
in: `int` request fields reject `bool` across V1+V2 via new
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kalshi-sdk"
version = "2.6.0"
version = "2.7.0"
description = "A professional Python SDK for the Kalshi prediction markets API"
readme = "README.md"
license = { text = "MIT" }
Expand Down
Loading