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

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

## 21.0.0 — 2026-10-10

Reconciles OpenAPI 3.33.0 → 3.34.0 and perps spec drift after nightly
contract failures (Closes #533). **Breaking** for constructors of
`MarginMarket` that omit the new required funding-limit fields.
Re-vendored `specs/openapi.yaml` (hash
`091f171718a0376b0c1a2e9348b02b1ce003d0786dd04d772dd11005ca414bf4`),
`specs/perps_openapi.yaml` (hash
`27e4b0b76a686f797342f1526a08df5b8f60d8104a81b65c94073d3d89b54308`), and
`specs/perps_scm_openapi.yaml` (hash
`3ac27696d43bffbb5e993daa7106e62872028b7630980a492438d1d0043202e8`).

### Changed (breaking)

- Perps **`MarginMarket.funding_deadband`** / **`funding_cap`** (required
`Decimal`, JSON numbers) and **`funding_limit_unit`** (required
`"percentage"` / `"dollar"`). Funding limits configured at market
creation, expressed in `funding_limit_unit`. Live `markets.list` /
`markets.get` callers are unaffected; constructors and fixtures must
pass the new fields.

### Added

- **`fcm.create_position_transfer(...)`** (sync + async) —
`POST /fcm/positions/transfers`. Moves a market position from a source
subtrader into a subtrader under the FCM's error account. Kalshi must
enable the endpoint for the FCM; until then the server returns 400.
Request body `CreateFCMPositionTransferRequest`: `source_subtrader_id`,
`target_subtrader_id`, `market_ticker`, `position_centicount` (contracts
× 100, non-zero; positive YES, negative NO), `price_centicents` (YES
price, 0–10000). Returns `CreateFCMPositionTransferResponse`. POST is
never retried; requests are not idempotent.
- Perps **`transfers.transfer_instance(..., client_transfer_id=)`** (sync +
async): optional UUID for duplicate prevention on
`IntraExchangeInstanceTransferRequest`. Omitted from the body when `None`.
Only supported for primary accounts.
- Perps **`MarginMarket.bid_size` / `ask_size`** (optional, `bid_size_fp` /
`ask_size_fp` on the wire): contracts resting at the best bid and ask.

### Changed

- Perps **`portfolio.trades` / `trades_all`**: `ticker` is now optional. Omit
it to fetch public margin trades across all markets. Existing calls that
pass `ticker` are unchanged.
- Perps fills, positions, and order `subaccount` no longer have a spec default
of `0`; the SDK already omits `subaccount` when `None`, which now means all
subaccounts (or the key's locked subaccount). Docs and tests updated.

### Spec notes

- Perps SCM large-trader positions rows gain `reportable_threshold`; that
surveillance endpoint is not part of the SDK surface.

## 20.0.0 — 2026-10-07

Reconciles perps OpenAPI drift after nightly contract failures (Closes #531).
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.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)
- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.34.0, 118 operations; 117 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 (116 mapped of 117 operations across 19 resources, OpenAPI v3.31.0) and WebSocket API (12 typed `subscribe_*` channels + escape-hatch).
- **Full coverage** of the Kalshi REST API (117 mapped of 118 operations across 19 resources, OpenAPI v3.34.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.
Expand Down
4 changes: 2 additions & 2 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** — 116 mapped of 117 operations across 19 resources
(OpenAPI v3.31.0), every kwarg drift-tested against the spec.
- **Full REST coverage** — 117 mapped of 118 operations across 19 resources
(OpenAPI v3.34.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 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

## v20.0 → v21.0.0

Reconciles OpenAPI 3.33.0 → 3.34.0 and perps spec drift (Closes #533).
**Breaking** for constructors of `MarginMarket` that omit the new required
funding-limit fields.

### Response model field changes

- **Perps `MarginMarket.funding_deadband`** — required `Decimal` (JSON
number). Funding deadband configured at market creation, expressed in
`funding_limit_unit`.
- **Perps `MarginMarket.funding_cap`** — required `Decimal` (JSON number).
Funding cap configured at market creation, expressed in
`funding_limit_unit`.
- **Perps `MarginMarket.funding_limit_unit`** — required `"percentage"` or
`"dollar"`. Unit for the funding limits.

Live `markets.list` / `markets.get` callers are unaffected; constructors
and fixtures must pass the new fields.

```python
# Before (constructors / test fixtures):
# MarginMarket(..., exchange_index=0, market_version=1)

# After:
MarginMarket(
...,
exchange_index=0,
market_version=1,
funding_deadband=0,
funding_cap=1,
funding_limit_unit="percentage",
)
```

### Added (non-breaking)

- `fcm.create_position_transfer(...)` (sync + async) —
`POST /fcm/positions/transfers`, plus `CreateFCMPositionTransferRequest` /
`CreateFCMPositionTransferResponse`.
- Optional `IntraExchangeInstanceTransferRequest.client_transfer_id` and
`perps.transfers.transfer_instance(..., client_transfer_id=)`.
- Optional `MarginMarket.bid_size` / `ask_size`.
- Perps `portfolio.trades` / `trades_all`: `ticker` is now optional.

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

## v19.0 → v20.0.0

Reconciles perps OpenAPI drift (Closes #531). **Breaking** for
Expand Down
15 changes: 14 additions & 1 deletion docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ Omit to skip the check.
`orders.create(..., market_id=)` / `orders.amend(..., market_id=)` is a
stable exchange market UUID. It may be supplied with `ticker` (ticker takes
precedence when both are set). Omit so the key is not sent.
`orders.create(..., subaccount=)` is optional (0 = primary). Omit so the
key is not sent.
`portfolio.fills()` / `portfolio.positions()` omit `subaccount` to return
rows across all subaccounts (subaccount-restricted API keys still only
see their locked subaccount). Pass `subaccount=0` for the primary.
`portfolio.trades()` / `trades_all()` take an optional `ticker`; omit it
to retrieve public trades across all margin markets.

!!! warning "Deprecated in v7.2.0 — `list_fcm` / `list_all_fcm`"
Kalshi removed `GET /margin/fcm/orders` from the perps OpenAPI. The SDK
Expand Down Expand Up @@ -160,7 +167,13 @@ perps.portfolio.cancel_cross_exit_trigger("BTC-PERP", trig.id)
carries the same fields as **required** (inherent to a settled historical
record). `MarginMarket.leverage_estimates` maps notional position sizes
(`"1000"`, `"10000"`, …) to `MultiplierDecimal` leverage, or `None` when margin
config / price data is unavailable.
config / price data is unavailable. `MarginMarket.bid_size` / `ask_size`
(`FixedPointCount | None`) are the resting size at the best bid/ask
(`bid_size_fp` / `ask_size_fp` on the wire); omitted when market stats are
unavailable. `MarginMarket.funding_deadband` / `funding_cap`
(`MultiplierDecimal`) and `funding_limit_unit` (`"percentage"` /
`"dollar"`) are required; they are the funding limits configured at
market creation, expressed in `funding_limit_unit`.
- **Mark prices** (since the v3.21.0 spec sync) — `MarginMarket` carries three
optional reference-price objects: `settlement_mark_price`,
`liquidation_mark_price`, and `reference_price`, each a nested `TickerPrice`
Expand Down
24 changes: 24 additions & 0 deletions docs/resources/fcm.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ calls come back 401/403. Auth required throughout.
| `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` |
| `create_position_transfer(*, source_subtrader_id, target_subtrader_id, market_ticker, position_centicount, price_centicents)` | `POST /fcm/positions/transfers` |
| `list_subtraders()` | `GET /fcm/subtraders` |
| `create_subtrader(*, subtrader_suffix)` | `POST /fcm/subtraders` |
| `blocked_categories(*, subtrader_id)` | `GET /fcm/subtraders/blocked_categories` |
Expand Down Expand Up @@ -93,6 +94,29 @@ for mp in client.fcm.positions_all(subtrader_id="st_alpha", settlement_status="u
[`portfolio.positions`](portfolio.md#positions) / `positions_all` (OpenAPI
3.32.0). Both endpoints default to `unsettled` when the kwarg is omitted.

## Position transfer

Move a position in one market from a source subtrader into a subtrader under
the FCM's error account. **Kalshi must enable this endpoint for your FCM**;
until then the server returns 400. POST is never retried. Requests are not
idempotent — after a timeout or 5xx, check the position before retrying.

`position_centicount` is contracts × 100 (positive moves YES, negative moves
NO) and must be non-zero. `price_centicents` is the YES price per contract
in centicents (0–10000), even when moving NO. Use `0` for YES or `10000`
for NO to move no cash.

```python
resp = client.fcm.create_position_transfer(
source_subtrader_id="acct_CUST123",
target_subtrader_id="erracct_ERR1",
market_ticker="KXEXAMPLE-26DEC31",
position_centicount=1000, # 10 YES
price_centicents=4500, # 45 cents
)
print(resp.position_transfer_id)
```

## Subtrader admin

List and create subtraders, block event categories, and set a daily
Expand Down
4 changes: 3 additions & 1 deletion docs/resources/portfolio.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ New in v10.0.0. History and detail for fund moves between the
`event_contract` and `margined` exchange instances. **Creating** a transfer
is still on the perps surface
(`PerpsClient.transfers.transfer_instance()`); these GETs are on the core
portfolio API.
portfolio API. The perps create body accepts an optional
`client_transfer_id` (UUID) for duplicate prevention; omit so the key is
not sent.

```python
page = client.portfolio.intra_exchange_transfers(limit=50)
Expand Down
5 changes: 5 additions & 0 deletions docs/retries.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ with KalshiClient.from_env() as client:
Same pattern as `client_order_id`, but for inter-subaccount money movement.
Reconcile via `subaccounts.list_transfers()`.

`PerpsClient.transfers.transfer_instance(..., client_transfer_id=)` is the
same UUID idempotency key for intra-exchange instance transfers. It is
optional: omit so the key is not sent and each request creates a new
transfer. Only supported for primary accounts.

### Cancels

`client.orders.cancel_v2(order_id)` and `client.orders.batch_cancel_v2(...)`
Expand Down
6 changes: 5 additions & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
Candlestick,
CreateApiKeyRequest,
CreateApiKeyResponse,
CreateFCMPositionTransferRequest,
CreateFCMPositionTransferResponse,
CreateFCMSubtraderRequest,
CreateFCMSubtraderResponse,
CreateMarketInMultivariateEventCollectionRequest,
Expand Down Expand Up @@ -253,6 +255,8 @@
"Candlestick",
"CreateApiKeyRequest",
"CreateApiKeyResponse",
"CreateFCMPositionTransferRequest",
"CreateFCMPositionTransferResponse",
"CreateFCMSubtraderRequest",
"CreateFCMSubtraderResponse",
"CreateMarketInMultivariateEventCollectionRequest",
Expand Down Expand Up @@ -427,4 +431,4 @@
"Withdrawal",
]

__version__ = "20.0.0"
__version__ = "21.0.0"
8 changes: 8 additions & 0 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ class ContractEntry:
sdk_model="kalshi.models.fcm.ListFCMSubtradersResponse",
spec_schema="ListFCMSubtradersResponse",
),
ContractEntry(
sdk_model="kalshi.models.fcm.CreateFCMPositionTransferRequest",
spec_schema="CreateFCMPositionTransferRequest",
),
ContractEntry(
sdk_model="kalshi.models.fcm.CreateFCMPositionTransferResponse",
spec_schema="CreateFCMPositionTransferResponse",
),
ContractEntry(
sdk_model="kalshi.models.fcm.CreateFCMSubtraderRequest",
spec_schema="CreateFCMSubtraderRequest",
Expand Down
4 changes: 4 additions & 0 deletions kalshi/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
WeeklySchedule,
)
from kalshi.models.fcm import (
CreateFCMPositionTransferRequest,
CreateFCMPositionTransferResponse,
CreateFCMSubtraderRequest,
CreateFCMSubtraderResponse,
FcmFill,
Expand Down Expand Up @@ -236,6 +238,8 @@
"Candlestick",
"CreateApiKeyRequest",
"CreateApiKeyResponse",
"CreateFCMPositionTransferRequest",
"CreateFCMPositionTransferResponse",
"CreateFCMSubtraderRequest",
"CreateFCMSubtraderResponse",
"CreateMarketInMultivariateEventCollectionRequest",
Expand Down
37 changes: 35 additions & 2 deletions kalshi/models/fcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,43 @@

from __future__ import annotations

from pydantic import AliasChoices, AwareDatetime, BaseModel, Field
from pydantic import AliasChoices, AwareDatetime, BaseModel, Field, field_validator

from kalshi.models.orders import SideLiteral
from kalshi.types import DollarDecimal, FixedPointCount
from kalshi.types import DollarDecimal, FixedPointCount, StrictInt


class CreateFCMPositionTransferRequest(BaseModel):
"""Body for POST /fcm/positions/transfers.

Moves a position in one market from a source subtrader into a subtrader
under the FCM's error account. ``position_centicount`` is contracts x 100
(positive = YES, negative = NO) and must be non-zero. ``price_centicents``
is the YES price per contract in centicents (cents x 100), 0-10000.
"""

source_subtrader_id: str
target_subtrader_id: str
market_ticker: str
position_centicount: StrictInt
price_centicents: StrictInt = Field(ge=0, le=10000)

model_config = {"extra": "forbid"}

@field_validator("position_centicount")
@classmethod
def _position_centicount_nonzero(cls, value: int) -> int:
if value == 0:
raise ValueError("position_centicount must be non-zero")
return value


class CreateFCMPositionTransferResponse(BaseModel):
"""Response from POST /fcm/positions/transfers."""

position_transfer_id: str

model_config = {"extra": "allow"}


class CreateFCMSubtraderRequest(BaseModel):
Expand Down
14 changes: 14 additions & 0 deletions kalshi/perps/models/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ class MarginMarket(BaseModel):
# Required market version (corporate-action counter). Pass to create() as
# market_version so the server rejects with 409 if the market changed.
market_version: int
# Funding limits configured at market creation. Spec-required JSON
# numbers (``type: number, format: double``); ``funding_limit_unit``
# is the unit those numbers are expressed in.
funding_deadband: MultiplierDecimal = Field(ge=0)
funding_cap: MultiplierDecimal = Field(ge=0)
funding_limit_unit: Literal["percentage", "dollar"]

leverage_estimate: MultiplierDecimal | None = None
# Leverage (1 / margin_rate) keyed by notional position size in dollars
Expand All @@ -103,7 +109,15 @@ class MarginMarket(BaseModel):
short_leverage_estimates: dict[str, MultiplierDecimal] | None = None
price: DollarDecimal | None = None
bid: DollarDecimal | None = None
bid_size: FixedPointCount | None = Field(
default=None,
validation_alias=AliasChoices("bid_size_fp", "bid_size"),
)
ask: DollarDecimal | None = None
ask_size: FixedPointCount | None = Field(
default=None,
validation_alias=AliasChoices("ask_size_fp", "ask_size"),
)
volume: FixedPointCount | None = Field(
default=None,
validation_alias=AliasChoices("volume_fp", "volume"),
Expand Down
3 changes: 2 additions & 1 deletion kalshi/perps/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ class CreateMarginOrderRequest(BaseModel):
``price``/``count`` wire names match the field names (no ``_dollars``/``_fp``
suffix), so ``by_alias=True`` is a no-op for them — confirmed against the spec.
``subaccount`` is carried in the *body* here (the cancel/decrease/amend
endpoints route it as a query param instead).
endpoints route it as a query param instead). Omit (``None``) so
``exclude_none`` drops the key; do not default to ``0``.
"""

model_config = ConfigDict(extra="forbid")
Expand Down
5 changes: 5 additions & 0 deletions kalshi/perps/models/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ class IntraExchangeInstanceTransferRequest(BaseModel):
Shard fields default to ``0`` (only ``0`` is currently supported); because
the body is serialized with ``exclude_none=True`` (not ``exclude_defaults``),
shard ``0`` values are emitted on the wire, which is fine.
``client_transfer_id`` is optional duplicate-prevention; omit (``None``)
so ``exclude_none`` drops the key. Only supported for primary accounts.
"""

source: ExchangeInstanceLiteral
destination: ExchangeInstanceLiteral
amount: StrictInt = Field(gt=0)
source_exchange_shard: StrictInt = Field(default=0, ge=0)
destination_exchange_shard: StrictInt = Field(default=0, ge=0)
# Client UUID for duplicate prevention. Omit (None) so exclude_none
# drops the key; each request then creates a new transfer.
client_transfer_id: UUID | None = None

model_config = {"extra": "forbid"}

Expand Down
Loading
Loading