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
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,67 @@

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

## 16.0.0 — 2026-09-26

Reconciles upstream OpenAPI **3.30.0 → 3.31.0** plus matching perps and
AsyncAPI updates after nightly contract failures (Closes #519).
**Breaking** for constructors of perps `FCMSubtraderRiskControls`,
`GetFCMSubtraderRiskControlsResponse`, and `NotionalRiskLimitResponse`
that omit newly required fields.

### Changed (breaking)

- **Perps** `FCMSubtraderRiskControls.current_im` (`Decimal`, required) —
initial margin currently attributable to the cap's scope.
- **Perps** `GetFCMSubtraderRiskControlsResponse.notional_limits`
(`list[FCMSubtraderNotionalRiskLimit]`, required) — the admin-set
notional value risk limits for the same subtrader.
- **Perps** `NotionalRiskLimitResponse.total_current_usage` (`Decimal`) and
`current_usage_by_market_ticker` (`dict[str, Decimal]`), both required.
Live `perps.fcm.risk_controls()` / `perps.margin.notional_risk_limit()`
callers are unaffected; tests/mocks that construct these models must pass
the new fields.

### Added

- Optional **`subaccount=`** on `historical.fills` / `fills_all` /
`orders` / `orders_all` (sync + async). Subaccount-restricted keys see
only their own subaccount; a supplied value must match the restriction.
- Optional **`max_updated_ts=`** on `markets.list` / `list_all`
(mirrors `min_updated_ts`).
- Optional **`key_type=`** (`"rsa"` / `"ed25519"`) on `api_keys.generate`
and `GenerateApiKeyRequest`; optional `GenerateApiKeyResponse.key_type`.
The SDK request signer (`KalshiAuth`) remains RSA-PSS only.
- **FCM fills** on `client.fcm`: `fills(min_ts=, max_ts=, cursor=)` →
`GetFcmFillsResponse`, and `fills_all(...)` yielding `FcmFill`
(`GET /fcm/fills`).
- **Perps** `fcm.update_notional_risk_limit(notional_value_risk_limit=)`
(`PUT /margin/fcm/notional_risk_limit`) and
`fcm.delete_notional_risk_limit()` (`DELETE`), with
`UpdateFCMNotionalRiskLimitRequest`. Never retried.
- **Perps** `FCMSubtraderNotionalRiskLimit` model; optional
`NotionalRiskLimitResponse.member_notional_value_risk_limit` and
`effective_account_notional_value_risk_limit`.

### Changed (non-breaking)

- `RestingMarginReservationLiteral` accepts `"none"`.
- Perps `LastUpdateReason` / `LastUpdateReasonLiteral` / WS
`PerpsLastUpdateReason` accept `"ReduceOnlyCancel"`.

### Spec notes

- Core OpenAPI `info.version` **3.31.0** (117 operations; 116 mapped).
Still unimplemented on the core client:
`POST /portfolio/intra_exchange_instance_transfer`.
- AsyncAPI still 15 channels. 12 typed `subscribe_*` helpers. The new
communications `user_filter` subscribe option and core user-order
`last_update_reason` field are not yet modeled.
- Perps OpenAPI: 48 → 50 operations.
- Upstream dropped the deprecated `Market.liquidity_dollars` and Klear
`prev_settlement_prices`; the SDK keeps its existing optional fields for
now.

## 15.0.0 — 2026-09-20

Reconciles upstream OpenAPI **3.29.0 → 3.30.0** plus matching perps, Klear,
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ tests/

## API Reference

- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.30.0, 116 operations; 115 mapped in the core SDK — `POST /portfolio/intra_exchange_instance_transfer` is implemented on `PerpsClient.transfers.transfer_instance` and left unimplemented on the core client)
- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.31.0, 117 operations; 116 mapped in the core SDK — `POST /portfolio/intra_exchange_instance_transfer` is implemented on `PerpsClient.transfers.transfer_instance` and left unimplemented on the core client)
- AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (15 WebSocket channels; 12 typed `subscribe_*` + escape-hatch)
- Base URL: https://api.elections.kalshi.com/trade-api/v2
- Demo URL: https://demo-api.kalshi.co/trade-api/v2
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/)

- **Full coverage** of the Kalshi REST API (115 mapped of 116 operations across 19 resources, OpenAPI v3.30.0) and WebSocket API (12 typed `subscribe_*` channels + escape-hatch).
- **Perps (margin) API**: standalone `PerpsClient` / `AsyncPerpsClient` + `PerpsWebSocket` for the perpetual-futures exchange (48 REST operations, 6 WS channels), plus a `KlearClient` for the Self-Clearing-Member "Klear" settlement API (26 of 27 operations). See [Perps (margin) trading](#perps-margin-trading).
- **Full coverage** of the Kalshi REST API (116 mapped of 117 operations across 19 resources, OpenAPI v3.31.0) and WebSocket API (12 typed `subscribe_*` channels + escape-hatch).
- **Perps (margin) API**: standalone `PerpsClient` / `AsyncPerpsClient` + `PerpsWebSocket` for the perpetual-futures exchange (50 REST operations, 6 WS channels), plus a `KlearClient` for the Self-Clearing-Member "Klear" settlement API (26 of 27 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, 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` / `cancel_all_v2` plus batched variants on `/portfolio/events/orders/*` — the only order-write surface.
- **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`.
Expand Down
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) prediction
markets API.

- **Full REST coverage** — 115 mapped of 116 operations across 19 resources
(OpenAPI v3.30.0), every kwarg drift-tested against the spec.
- **Full REST coverage** — 116 mapped of 117 operations across 19 resources
(OpenAPI v3.31.0), every kwarg drift-tested against the spec.
- **V2 event-market orders** — new `create_v2` / `amend_v2` / `decrease_v2` /
`cancel_v2` / `cancel_all_v2` family on `/portfolio/events/orders/*`. Legacy `/portfolio/orders`
keeps working; deprecation no earlier than May 6, 2026.
Expand All @@ -16,7 +16,7 @@ markets API.
channels), backpressure strategies, and an in-memory orderbook builder.
Async-only — access via `AsyncKalshiClient.ws`.
- **Perps (margin) API** — standalone `PerpsClient` / `AsyncPerpsClient` +
`PerpsWebSocket` for the perpetual-futures exchange (48 REST operations, 6 WS
`PerpsWebSocket` for the perpetual-futures exchange (50 REST operations, 6 WS
channels), and a `KlearClient` for the Self-Clearing-Member settlement API
(26 of 27 operations, Bearer token auth). See [Perps](perps.md).
- **FIX protocol** — a hand-rolled, async-first FIX engine (FIXT.1.1 / FIX50SP2)
Expand Down
48 changes: 48 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
# Migration

## v15.0 → v16.0.0

Reconciles upstream OpenAPI **3.30.0 → 3.31.0** plus matching perps and
AsyncAPI updates (Closes #519). **Breaking** only for code that constructs
perps `FCMSubtraderRiskControls`, `GetFCMSubtraderRiskControlsResponse`, or
`NotionalRiskLimitResponse` without the new required fields.

### Response model field changes

- **Perps** `FCMSubtraderRiskControls.current_im` — required `Decimal`.
- **Perps** `GetFCMSubtraderRiskControlsResponse.notional_limits` —
required `list[FCMSubtraderNotionalRiskLimit]`.
- **Perps** `NotionalRiskLimitResponse.total_current_usage` (required
`Decimal`) and `current_usage_by_market_ticker` (required
`dict[str, Decimal]`).

```python
# Before (constructors / test fixtures):
# FCMSubtraderRiskControls(subtrader_id="u_desk1", im_cap="100.0000")
# GetFCMSubtraderRiskControlsResponse(risk_controls=[...])
# NotionalRiskLimitResponse(default_notional_value_risk_limit="5000.0000",
# notional_value_risk_limits_by_market_ticker={})

# After:
FCMSubtraderRiskControls(subtrader_id="u_desk1", im_cap="100.0000", current_im="42.0000")
GetFCMSubtraderRiskControlsResponse(risk_controls=[...], notional_limits=[])
NotionalRiskLimitResponse(
default_notional_value_risk_limit="5000.0000",
notional_value_risk_limits_by_market_ticker={},
total_current_usage="0",
current_usage_by_market_ticker={},
)
```

Live `perps.fcm.risk_controls()` / `perps.margin.notional_risk_limit()`
callers are unaffected.

### Added (non-breaking)

- `historical.fills` / `fills_all` / `orders` / `orders_all(..., subaccount=)`
- `markets.list` / `list_all(..., max_updated_ts=)`
- `api_keys.generate(..., key_type=)` (the SDK signer stays RSA-only)
- `fcm.fills()` / `fcm.fills_all()`
- Perps `fcm.update_notional_risk_limit()` / `delete_notional_risk_limit()`

See the [changelog](https://github.com/TexasCoding/kalshi-python-sdk/blob/main/CHANGELOG.md)
for the full list.

## v14.0 → v15.0.0

Reconciles upstream OpenAPI **3.29.0 → 3.30.0** plus matching perps, Klear,
Expand Down
14 changes: 13 additions & 1 deletion docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async with AsyncPerpsClient.from_env(demo=True) as perps:
| `margin` | `balance()`, `risk()`, `notional_risk_limit()`, `fee_tiers()`, `fee_tier_rates()`, `api_limits()` |
| `funding` | `rate_estimate()`, `historical_rates()`, `history()` |
| `transfers` | `transfer_instance()`, `create_subaccount()`, `transfer_subaccount()` |
| `fcm` | `create_subtrader(subtrader_suffix=...)`; `risk_controls` / `update_risk_controls` / `delete_risk_controls` |
| `fcm` | `create_subtrader(subtrader_suffix=...)`; `risk_controls` / `update_risk_controls` / `delete_risk_controls`; `update_notional_risk_limit` / `delete_notional_risk_limit` |

The margin order side is `bid` / `ask` (not the prediction API's `yes` / `no`).
Orders create/cancel/decrease/amend are POSTs/DELETEs and are **never retried**.
Expand Down Expand Up @@ -94,8 +94,20 @@ perps.fcm.update_risk_controls(
perps.fcm.delete_risk_controls(subtrader_id="user_desk1", market_ticker="BTC-PERP")
# asset_class is mutually exclusive with market_ticker
perps.fcm.risk_controls(subtrader_id="user_desk1", asset_class="Crypto")

# Member-set account notional limit (PUT/DELETE are never retried).
# The exchange enforces the smaller of this value and the Kalshi-set limit.
perps.fcm.update_notional_risk_limit(notional_value_risk_limit=Decimal("5000.0000"))
perps.fcm.delete_notional_risk_limit()
```

`risk_controls()` also returns `notional_limits` (admin-set notional caps for
the same subtrader). Each `FCMSubtraderRiskControls` row includes required
`current_im`. `margin.notional_risk_limit()` reports required
`total_current_usage` and `current_usage_by_market_ticker`, plus optional
`member_notional_value_risk_limit` and
`effective_account_notional_value_risk_limit`.

Exit triggers (stop-loss / take-profit / trailing) sit on a position slot:

```python
Expand Down
10 changes: 8 additions & 2 deletions docs/resources/api-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Auth required throughout (you need an existing key to manage keys).
|---|---|
| `list(*, fcm_subtrader_id=None)` | `GET /api_keys` |
| `create(*, name, public_key, scopes=None, subaccount=None, fcm_subtrader_id=None)` | `POST /api_keys` |
| `generate(*, name, scopes=None, subaccount=None, fcm_subtrader_id=None)` | `POST /api_keys/generate` |
| `generate(*, name, scopes=None, subaccount=None, fcm_subtrader_id=None, key_type=None)` | `POST /api_keys/generate` |
| `delete(api_key)` | `DELETE /api_keys/{api_key}` |

!!! note "Subaccount-scoped keys (spec v3.23.0)"
Expand Down Expand Up @@ -44,10 +44,16 @@ The simplest path — Kalshi mints the keypair, you store the private key once:
```python
resp = client.api_keys.generate(name="ci-bot-2026", scopes=["read", "write"])
private_pem = resp.private_key.get_secret_value() # SecretStr — see warning
print(resp.api_key.api_key) # the key id
print(resp.api_key_id) # the key id
# Persist private_pem somewhere safe; you will not see it again.
```

`key_type` is `"rsa"` or `"ed25519"`. Omit it and the server mints RSA, which
is what `KalshiAuth` can sign with. An Ed25519 private key (`key_type="ed25519"`,
PKCS#8 PEM) is returned the same way, but this SDK's request signer is
RSA-PSS only — it cannot authenticate calls with that key. `resp.key_type`
echoes the algorithm when the server sends it.

!!! danger "`private_key` is a `SecretStr` — and you only see it once"
`resp.private_key` is a `pydantic.SecretStr`. `print(resp.private_key)`
will print `**********`, **not** the key. Use `.get_secret_value()` to
Expand Down
19 changes: 19 additions & 0 deletions docs/resources/fcm.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ calls come back 401/403. Auth required throughout.
|---|---|
| `orders(*, subtrader_id=None, client_order_ids=None, ...)` | `GET /fcm/orders` |
| `orders_all(*, subtrader_id=None, client_order_ids=None, ...)` | walks `orders` |
| `fills(*, min_ts=None, max_ts=None, cursor=None)` | `GET /fcm/fills` |
| `fills_all(*, min_ts=None, max_ts=None, max_pages=None)` | walks `fills` |
| `positions(*, subtrader_id, ...)` | `GET /fcm/positions` |
| `list_subtraders()` | `GET /fcm/subtraders` |
| `create_subtrader(*, subtrader_suffix)` | `POST /fcm/subtraders` |
Expand Down Expand Up @@ -43,6 +45,23 @@ for o in client.fcm.orders_all(subtrader_id="st_alpha", status="resting"):
Same `Order` model as [Orders](orders.md). Standard `Page[Order]` pagination
on `orders()`.

## Fills

Fills across the member's subtraders. Query params are only `min_ts`,
`max_ts`, and `cursor` — there is no `limit` or `subtrader_id` filter.
`fills()` returns `GetFcmFillsResponse` (`fills`, `cursor`). `fills_all()`
walks that cursor and yields each `FcmFill`. Prices are `Decimal`
(`yes_price` accepts `yes_price_dollars`); `count` accepts `count_fp`.

```python
resp = client.fcm.fills(min_ts=1_700_000_000, max_ts=1_800_000_000)
for fill in resp.fills:
print(fill.fill_id, fill.ticker, fill.taker_outcome_side, fill.yes_price, fill.count)

for fill in client.fcm.fills_all(min_ts=1_700_000_000):
print(fill.maker_subtrader_id, fill.taker_subtrader_id, fill.maker_fee_cost)
```

## Positions

```python
Expand Down
16 changes: 12 additions & 4 deletions docs/resources/historical.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ analytics; live trading needs the real-time surfaces.
| `market(ticker)` | `GET /historical/markets/{ticker}` | no |
| `candlesticks(ticker, *, start_ts, end_ts, period_interval)` | `GET /historical/markets/{ticker}/candlesticks` | no |
| `trades(...)` / `trades_all(...)` | `GET /historical/trades` | no |
| `fills(...)` / `fills_all(...)` | `GET /historical/fills` | **yes** |
| `orders(...)` / `orders_all(...)` | `GET /historical/orders` | **yes** |
| `fills(..., subaccount=None)` / `fills_all(..., subaccount=None)` | `GET /historical/fills` | **yes** |
| `orders(..., subaccount=None)` / `orders_all(..., subaccount=None)` | `GET /historical/orders` | **yes** |
| `positions(*, subaccount=None, ...)` / `positions_all(*, subaccount=None, ...)` | `GET /historical/positions` | **yes** |

## Cutoff
Expand Down Expand Up @@ -67,13 +67,21 @@ trades = client.historical.trades(
Both require auth — these are your own trade history.

```python
for fill in client.historical.fills_all(ticker="KXPRES-24-DJT", min_ts=1_600_000_000):
print(fill.fill_id, fill.price, fill.count)
for fill in client.historical.fills_all(
ticker="KXPRES-24-DJT",
min_ts=1_600_000_000,
subaccount=1, # omit to include every subaccount
):
print(fill.fill_id, fill.count)

for order in client.historical.orders_all(ticker="KXPRES-24-DJT", min_ts=1_600_000_000):
print(order.order_id, order.client_order_id)
```

`subaccount` is optional on both fills and orders (`0` is the primary subaccount).
Omit it to include every subaccount. A key restricted to one subaccount still
sees only that subaccount, and a supplied value must match the restriction.

## Historical positions

Auth required. Settled market positions archived to the historical database
Expand Down
8 changes: 5 additions & 3 deletions docs/resources/markets.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ for market in page:
for the wire (this endpoint uses comma-join form, **not** the explode form
used by `bulk_orderbooks`).

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.
All eight `*_ts` filters (`min_created_ts`, `max_created_ts`, `min_updated_ts`,
`max_updated_ts`, `min_close_ts`, `max_close_ts`, `min_settled_ts`,
`max_settled_ts`) are Unix-second ints. `max_updated_ts` mirrors
`min_updated_ts`: metadata updated no later than that timestamp. It combines
with `min_updated_ts` and `mve_filter=exclude` the same way.

`list_all(...)` walks cursors and returns an iterator — see
[Pagination](../pagination.md).
Expand Down
8 changes: 7 additions & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
AmendOrderV2Response,
Announcement,
ApiKey,
ApiKeyTypeLiteral,
ApiUsageLevelGrant,
ApplySubaccountTransferRequest,
AssociatedEvent,
Expand Down Expand Up @@ -84,6 +85,7 @@
ExchangeIndexStatus,
ExchangeInstanceLiteral,
ExchangeStatus,
FcmFill,
FCMSubtrader,
Fill,
ForecastPercentilesPoint,
Expand All @@ -94,6 +96,7 @@
GetCommunicationsIDResponse,
GetEventLiveDataResponse,
GetFCMEventContractDailyCapResponse,
GetFcmFillsResponse,
GetFCMSubtraderBlockedCategoriesResponse,
GetFiltersBySportsResponse,
GetGameStatsResponse,
Expand Down Expand Up @@ -223,6 +226,7 @@
"AmendOrderV2Response",
"Announcement",
"ApiKey",
"ApiKeyTypeLiteral",
"ApiUsageLevelGrant",
"ApplySubaccountTransferRequest",
"AssociatedEvent",
Expand Down Expand Up @@ -279,6 +283,7 @@
"ExchangeInstanceLiteral",
"ExchangeStatus",
"FCMSubtrader",
"FcmFill",
"Fill",
"FixClient",
"FixConfig",
Expand All @@ -293,6 +298,7 @@
"GetEventLiveDataResponse",
"GetFCMEventContractDailyCapResponse",
"GetFCMSubtraderBlockedCategoriesResponse",
"GetFcmFillsResponse",
"GetFiltersBySportsResponse",
"GetGameStatsResponse",
"GetIncentiveProgramsResponse",
Expand Down Expand Up @@ -421,4 +427,4 @@
"Withdrawal",
]

__version__ = "15.0.0"
__version__ = "16.0.0"
17 changes: 17 additions & 0 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ class ContractEntry:
sdk_model="kalshi.models.fcm.UpdateFCMEventContractDailyCapRequest",
spec_schema="UpdateFCMEventContractDailyCapRequest",
),
ContractEntry(
sdk_model="kalshi.models.fcm.FcmFill",
spec_schema="FcmFill",
notes="yes_price/count use short names with _dollars/_fp aliases",
),
ContractEntry(
sdk_model="kalshi.models.fcm.GetFcmFillsResponse",
spec_schema="GetFcmFillsResponse",
),
ContractEntry(
sdk_model="kalshi.models.multivariate.MultivariateEventCollection",
spec_schema="MultivariateEventCollection",
Expand Down Expand Up @@ -901,6 +910,14 @@ class ContractEntry:
sdk_model="kalshi.perps.models.fcm.UpdateFCMSubtraderRiskControlsRequest",
spec_schema="UpdateFCMSubtraderRiskControlsRequest",
),
ContractEntry(
sdk_model="kalshi.perps.models.fcm.FCMSubtraderNotionalRiskLimit",
spec_schema="FCMSubtraderNotionalRiskLimit",
),
ContractEntry(
sdk_model="kalshi.perps.models.fcm.UpdateFCMNotionalRiskLimitRequest",
spec_schema="UpdateFCMNotionalRiskLimitRequest",
),
ContractEntry(
sdk_model="kalshi.perps.models.portfolio.ExitTrigger",
spec_schema="ExitTrigger",
Expand Down
Loading
Loading