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
19 changes: 16 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ prediction-API surface. Additive release: no changes to `KalshiClient`.
exchange (`external-api.kalshi.com` / demo `external-api.demo.kalshi.co`,
`/trade-api/v2`), with their own `PerpsConfig` and a separate `KALSHI_PERPS_*`
credential namespace. They reuse the prediction-API RSA-PSS signer and HTTP
transport unchanged. Resource families: `exchange` (status / enabled gate /
transport unchanged. The constructors and `from_env()` also accept
`ws_base_url` (set the WS endpoint independently from REST) and `password`
(passphrase for an encrypted key), and read `KALSHI_PERPS_WS_BASE_URL` /
`KALSHI_PERPS_PRIVATE_KEY_PASSPHRASE` from the environment; passing `config=`
together with `demo`/`base_url`/`ws_base_url` is rejected. Resource families:
`exchange` (status / enabled gate /
risk parameters), `markets` (list / get / orderbook / candlesticks), `orders`
(create / get / list / cancel / decrease / amend + FCM), `order_groups`,
`portfolio` (positions / fills / trades), `margin` (balance / risk /
Expand Down Expand Up @@ -52,8 +57,16 @@ prediction-API surface. Additive release: no changes to `KalshiClient`.
spec-required array key raises `ValidationError` (surfacing spec drift instead
of silently returning `[]`), while a **null** array coerces to `[]` (Kalshi's
empty-as-null convention — the prior `data.get(...)` extraction would
`TypeError` on a null array). The equivalent perps endpoints were hardened the
same way (markets/funding list responses).
`TypeError` on a null array). The perps `markets.list` / `markets.candlesticks`
/ `funding.historical_rates` / `funding.history` responses use the same
`NullableList` envelopes, so null-handling is consistent across both surfaces.
The optional `order_groups.list` stays tolerant of a missing/null array.

### Fixed

- `KalshiWebSocket._stop()` now retrieves an already-finished receive-loop's
exception, so a session torn down after a permanent close no longer logs
asyncio's "Task exception was never retrieved" on garbage collection.

### Internal

Expand Down
11 changes: 11 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Shipped

- **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),
alongside the unchanged prediction-API surface. Also hardened the prediction
and perps list endpoints to validate typed response envelopes (a missing
spec-required array now surfaces as a `ValidationError`). Additive for
existing `KalshiClient` users.
- **v3.1.0 (2026-06-04)** — OpenAPI sync v3.19.0 → v3.20.0: `events.fee_changes`,
`is_block_trade`, and the `event_fee_update` frame on the `market_lifecycle_v2`
WebSocket channel.
- **v3.0.1 (2026-05-26)** — OpenAPI sync v3.18.0 → v3.19.0.
- **v3.0.0 (2026-05-22)** — first major release in the v3 line. Three
breaking-rename issues (`#348`, `#349`, `#351`) deferred from the v2.7.0
audit closure land with one-release deprecation aliases:
Expand Down
2 changes: 2 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ from this repo can publish without further configuration.
## Cutting a release

1. Bump `version` in `pyproject.toml` and `__version__` in `kalshi/__init__.py`.
The `## <version>` heading in `CHANGELOG.md` must use the same version (the
workflow extracts the section by that heading for the release body).
2. Add a section to `CHANGELOG.md` for the new version. The release workflow
extracts the section between `## <version>` and the next `## ` heading
and uses it as the GitHub Release body, so write it for that audience.
Expand Down
10 changes: 8 additions & 2 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,14 @@ with KalshiClient.from_env() as client:
...
```

An explicit `password=` argument always wins over
`KALSHI_PRIVATE_KEY_PASSPHRASE` when both are supplied. A wrong passphrase
`KalshiClient.from_env()` has no `password=` kwarg — it reads the passphrase
only from `KALSHI_PRIVATE_KEY_PASSPHRASE`. For programmatic passphrase control,
build a `KalshiAuth` (above) and pass it as `auth=`. (The perps
`PerpsClient.from_env()` *does* accept a `password=` kwarg.)

Wherever a `password=` argument is accepted (the `KalshiAuth.from_*`
constructors and `PerpsClient`), it always wins over an environment passphrase
when both are supplied. A wrong passphrase
raises `KalshiAuthError` ("Invalid PEM private key…"); a missing passphrase
for an encrypted PEM raises `KalshiAuthError` pointing at the `password=`
parameter.
Expand Down
7 changes: 5 additions & 2 deletions docs/dataframes.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ print(df[["ticker", "yes_bid", "yes_ask", "volume_24h"]].head())

## How it works

Both methods walk `page.items` and call `item.model_dump(mode="python")` on
each, then hand the records to `pd.DataFrame(...)` / `pl.DataFrame(...)`.
Both methods build a column-oriented `dict[field, list[value]]` over
`page.items` (`getattr` per field — deliberately avoiding a per-row
`model_dump`) and hand it to `pd.DataFrame(...)` / `pl.DataFrame(...)`. Nested
model cells are dumped to dicts per column (so polars can infer a `Struct`
dtype) while scalar cells pass through unchanged.

This means:

Expand Down
30 changes: 29 additions & 1 deletion docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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_DEMO` | `false` | Truthy values (`true`, `1`, `yes`, `on`, case-insensitive) flip the client to the demo URLs. |
| `KALSHI_PRIVATE_KEY_PASSPHRASE` | unset | Passphrase for an encrypted PEM (read by `try_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). |

Expand Down Expand Up @@ -52,3 +53,30 @@ config = (
with KalshiClient.from_env(config=config) as client:
...
```

## Perps environment variables

`PerpsClient.from_env()` / `AsyncPerpsClient.from_env()` read a **separate**
`KALSHI_PERPS_*` namespace — perps requires a key issued for the perps exchange:

| Variable | Default | Effect |
|---|---|---|
| `KALSHI_PERPS_KEY_ID` | unset | Perps key ID. Omit for an unauthenticated client. |
| `KALSHI_PERPS_PRIVATE_KEY` | unset | PEM string. Conflicts with `KALSHI_PERPS_PRIVATE_KEY_PATH`. |
| `KALSHI_PERPS_PRIVATE_KEY_PATH` | unset | Path to a PEM file. |
| `KALSHI_PERPS_PRIVATE_KEY_PASSPHRASE` | unset | Passphrase for an encrypted perps PEM. A `password=` kwarg to the client overrides it. |
| `KALSHI_PERPS_DEMO` | `false` | `true` (case-insensitive) selects the demo REST + WS endpoints. |
| `KALSHI_PERPS_API_BASE_URL` | unset | Overrides the perps REST `base_url`. |
| `KALSHI_PERPS_WS_BASE_URL` | unset | Overrides the perps `ws_base_url`, so a REST override doesn't leave the WS feed on production. |
| `KALSHI_PERPS_ALLOW_UNKNOWN_HOST` | unset | Set to `1` to allow non-Kalshi perps hosts (mock servers / proxies). |

## Klear (SCM) environment variables

`KlearClient` / `AsyncKlearClient` use cookie-session auth (no RSA keys), so they
read only environment selectors:

| Variable | Default | Effect |
|---|---|---|
| `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. |
4 changes: 3 additions & 1 deletion docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ never collide with the prediction-API `KALSHI_*` vars:
|---|---|
| `KALSHI_PERPS_KEY_ID` | perps API key id (omit for an unauthenticated client) |
| `KALSHI_PERPS_PRIVATE_KEY` / `KALSHI_PERPS_PRIVATE_KEY_PATH` | RSA private key (PEM string or file) |
| `KALSHI_PERPS_API_BASE_URL` | optional base-URL override |
| `KALSHI_PERPS_PRIVATE_KEY_PASSPHRASE` | optional passphrase for an encrypted PEM (a `password=` kwarg overrides it) |
| `KALSHI_PERPS_API_BASE_URL` | optional REST base-URL override |
| `KALSHI_PERPS_WS_BASE_URL` | optional WS base-URL override (keeps the WS feed off production when overriding REST) |
| `KALSHI_PERPS_DEMO` | `"true"` routes to the demo exchange |

```python
Expand Down
10 changes: 4 additions & 6 deletions docs/resources/markets.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@ for market in page:
for the wire (this endpoint uses comma-join form, **not** the explode form
used by `bulk_orderbooks`).

All the `*_ts` filters (`min_close_ts`, `max_close_ts`, `min_open_ts`,
`max_open_ts`, `min_settle_ts`, `max_settle_ts`, `min_volume_24h`,
`min_total_volume`, `min_total_open_interest`) are Unix-second ints. Pair them
with `min_open_ts`/`max_open_ts` etc. as needed.
All seven `*_ts` filters (`min_created_ts`, `max_created_ts`, `min_updated_ts`,
`min_close_ts`, `max_close_ts`, `min_settled_ts`, `max_settled_ts`) are
Unix-second ints.

`list_all(...)` walks cursors and returns an iterator — see
[Pagination](../pagination.md).
Expand Down Expand Up @@ -94,7 +93,7 @@ candles = client.markets.candlesticks(
period_interval=60, # seconds: 60, 3600, 86400
include_latest_before_start=True,
)
for c in candles.candlesticks:
for c in candles: # list[Candlestick]
print(c.end_period_ts, c.yes_bid)
```

Expand All @@ -106,7 +105,6 @@ for c in candles.candlesticks:
```python
batches = client.markets.bulk_candlesticks(
market_tickers=["KXPRES-24-DJT", "KXPRES-24-KH"], # 1–100 tickers
series_ticker="KXPRES", # optional shard hint
start_ts=1_700_000_000,
end_ts=1_700_100_000,
period_interval=3600,
Expand Down
9 changes: 8 additions & 1 deletion docs/resources/multivariate.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ with a list of leg selections mints a derived YES/NO contract.
Public listing, auth-required minting. Attribute name on the client:
`multivariate_collections`.

!!! warning "Deprecated methods"
`create_market()`, `lookup_tickers()`, and `lookup_history()` are
deprecated — "This endpoint predates RFQs and should not be used for new
integrations." Calling them emits a `DeprecationWarning`. Use the
[Communications (RFQ/Quote)](communications.md) surface instead. `list()` /
`list_all()` / `get()` remain supported.

## Quick reference

| Method | Endpoint | Auth |
|---|---|---|
| `list(...)` / `list_all(...)` | `GET /multivariate_event_collections` | no |
| `get(collection_ticker)` | `GET /multivariate_event_collections/{ticker}` | no |
| `create_market(collection_ticker, *, selected_markets, with_market_payload=False)` | `POST /multivariate_event_collections/{ticker}` | yes |
| `lookup_tickers(collection_ticker, *, selected_markets)` | `PUT /multivariate_event_collections/{ticker}/lookup` | no |
| `lookup_tickers(collection_ticker, *, selected_markets)` | `PUT /multivariate_event_collections/{ticker}/lookup` | yes |
| `lookup_history(collection_ticker, *, lookback_seconds)` | `GET /multivariate_event_collections/{ticker}/lookup` (with `lookback_seconds` query param) | no |

## List collections
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/series.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ hist = client.series.forecast_percentile_history(
end_ts=1_700_100_000,
period_interval=86400,
)
for point in hist.forecast_percentiles:
for point in hist: # list[ForecastPercentilesPoint]
print(point.ts, point.percentile, point.value)
```

Expand Down
8 changes: 4 additions & 4 deletions docs/resources/subaccounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ below).

```python
resp = client.subaccounts.list_balances()
for bal in resp.balances:
print(bal.subaccount_number, bal.balance, bal.portfolio_value, bal.updated_ts)
for bal in resp.subaccount_balances:
print(bal.subaccount_number, bal.balance, bal.updated_ts)
```

`bal.balance` and `bal.portfolio_value` are **integer cents**.
`bal.updated_ts` is Unix seconds (not ISO datetime).
`bal.balance` is a `DollarDecimal` (dollars). `bal.updated_ts` is Unix seconds
(not ISO datetime).

## List transfers

Expand Down
4 changes: 2 additions & 2 deletions docs/retries.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ duplicate orders happen.

| Verb | Retries on |
|---|---|
| `GET`, `HEAD`, `OPTIONS` | `429`, `500`, `502`, `503`, `504` |
| `POST`, `DELETE`, `PUT`, `PATCH` | **never** |
| `GET`, `HEAD`, `OPTIONS` | `408`, `425`, `429`, `500`, `502`, `503`, `504`, `520`, `521`, `522`, `523`, `524` |
| `POST`, `PUT`, `DELETE` | **never** |

POST/DELETE/PUT are write paths. Retrying them risks duplicate orders,
duplicate cancels, duplicate transfers — even with `client_order_id`, you
Expand Down
9 changes: 5 additions & 4 deletions docs/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ for `user_order`, `market_position`, and the `multivariate_lookup` /
`multivariate` mismatch.

!!! warning "Migration (v3.1.0): `event_fee_update` rides `market_lifecycle_v2`"
Since v3.20.0 the `market_lifecycle_v2` channel also emits
`event_fee_update` frames (event-level fee override set or cleared), so
Since the v3.20.0 spec sync (SDK v3.1.0) the `market_lifecycle_v2` channel
also emits `event_fee_update` frames (event-level fee override set or
cleared), so
`subscribe_market_lifecycle()` now yields
`MarketLifecycleMessage | EventFeeUpdateMessage`. **Existing consumers must
discriminate on `.type` before touching payload fields** — an
Expand Down Expand Up @@ -425,8 +426,8 @@ silently corrupting book state.

| Strategy | Use for | Why |
|---|---|---|
| `DROP_OLDEST` | Read-only / latest-wins feeds: `ticker`, `trade`, `market_lifecycle`, `multivariate*` | Newest snapshot is always correct; an evicted old tick is recoverable from the next one. |
| `ERROR` | Stateful, sequenced feeds: `orderbook_delta`, `order_group_updates`, `user_orders` | A dropped delta corrupts derived state. Surface the backpressure to the consumer rather than continuing on a corrupted book. |
| `DROP_OLDEST` | Read-only / coalesced feeds: `ticker`, `trade`, `market_lifecycle`, `multivariate*`, `user_orders` | Newest sample is the one that matters; an evicted old frame is recoverable from the next one. |
| `ERROR` | Stateful, sequenced feeds: `orderbook_delta`, `order_group_updates` | A dropped delta corrupts derived state (the reconstructed book / order-group tracking). Surface the backpressure to the consumer rather than continuing on corrupted state. |

`ERROR` is fatal — the recv loop broadcasts sentinels and exits when it fires
(see [Backpressure](#backpressure)). Wire `on_error=` / `on_state_change=` to
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "kalshi-sdk"
version = "3.2.0"
description = "A professional Python SDK for the Kalshi prediction markets API"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.12"
Expand Down