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

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

## 18.0.0 — 2026-10-03

Reconciles upstream OpenAPI **3.32.0** content drift plus perps/AsyncAPI
updates after nightly contract failures (Closes #527). **Breaking** for
constructors of perps `MarginMarket` that omit the new required
`market_version`. Re-vendored `specs/openapi.yaml` (hash
`fc70d406efd7a27dfff117ae1e509d44c2d01db57c1c9613a3fbb1a69caf2c88`),
`specs/asyncapi.yaml` (hash
`304956d30c986b02e8a375b004f30f6bca07e484a0a6cf3c45eb32bf01c4ba19`), and
`specs/perps_openapi.yaml` (hash
`d13cb9c5c18cbb9ab2fe60d173c74511dea627a89321d17f7b0505a88f82aeb0`).

### Changed (breaking)

- Perps **`MarginMarket.market_version`** (required `int`) — market version
counter (starts at 1; increases on corporate actions). Live list/get callers
are unaffected; tests/mocks that construct `MarginMarket` must pass it.

### Added

- Optional **`market_version`** on perps `CreateMarginOrderRequest` / `orders.create`
(sync + async). If set and the market's current version differs, server rejects
with HTTP 409 / `market_version_mismatch`. Omit or leave unset to skip the check
(server default 0).

### Spec notes

- Core OpenAPI `info.version` still **3.32.0** (fills ticker description now allows
comma-separated list up to 100; RFQ obscure_creator_id docs clarified).
- AsyncAPI `lastUpdateReason` adds `SettlementBoundsCancel`.

## 17.1.0 — 2026-09-30

Reconciles upstream OpenAPI **3.31.0 → 3.32.0** content drift plus matching
Expand Down
34 changes: 34 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# Migration

## v17.1 → v18.0.0

Reconciles upstream OpenAPI **3.32.0** content drift plus matching perps and
AsyncAPI updates (Closes #527). **Breaking** only for code that constructs
perps `MarginMarket` without `market_version`.

### Response model field changes

- **Perps `MarginMarket.market_version`** — required `int`. Starts at 1 and
can increase on corporate actions (e.g. a stock split). Pass it as
`market_version` when creating an order so the server rejects with HTTP
409 / `market_version_mismatch` if the market changed since you read it.
Live `markets.list` / `markets.get` callers are unaffected; constructors
and fixtures must pass the new field.

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

# After:
MarginMarket(..., exchange_index=0, market_version=1)
```

### Added (non-breaking)

- Optional `CreateMarginOrderRequest.market_version` and
`perps.orders.create(..., market_version=)` (sync + async). If set and
the market's current version differs, the server rejects with HTTP 409 /
`market_version_mismatch`. Omit or leave unset to skip the check (server
default 0).

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

## v17.0 → v17.1.0

Reconciles upstream OpenAPI **3.32.0** (Closes #525). **Non-breaking**
Expand Down
4 changes: 4 additions & 0 deletions docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ 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**.
`orders.amend(..., expiration_time=)` is int64 Unix seconds: omit it to keep
the current expiry, or pass `0` to clear it (good-till-canceled).
`orders.create(..., market_version=)` is the expected market version (starts
at 1 on `MarginMarket.market_version`). If set and the market's current
version differs, the server rejects with HTTP 409 / `market_version_mismatch`.
Omit to skip the check.

!!! 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
2 changes: 1 addition & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,4 +427,4 @@
"Withdrawal",
]

__version__ = "17.1.0"
__version__ = "18.0.0"
3 changes: 3 additions & 0 deletions kalshi/perps/models/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class MarginMarket(BaseModel):
# Required exchange shard for order-group membership (markets and order
# groups must share the same exchange_index).
exchange_index: int
# 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

leverage_estimate: MultiplierDecimal | None = None
# Leverage (1 / margin_rate) keyed by notional position size in dollars
Expand Down
4 changes: 4 additions & 0 deletions kalshi/perps/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ class CreateMarginOrderRequest(BaseModel):
reduce_only: bool | None = None
subaccount: StrictInt | None = Field(default=None, ge=0)
order_group_id: str | None = None
# Expected market version. If set and the market's current version differs,
# the server rejects with HTTP 409 / market_version_mismatch. Omit (None)
# to skip the check — do not default to 0 so exclude_none omits the key.
market_version: StrictInt | None = None


class DecreaseMarginOrderRequest(BaseModel):
Expand Down
14 changes: 13 additions & 1 deletion kalshi/perps/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def _build_create_body(
reduce_only: bool | None,
subaccount: int | None,
order_group_id: str | None,
market_version: int | None,
) -> dict[str, Any]:
_check_request_exclusive(
request,
Expand All @@ -102,6 +103,7 @@ def _build_create_body(
reduce_only=reduce_only,
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
)
if request is None:
if (
Expand Down Expand Up @@ -132,6 +134,7 @@ def _build_create_body(
reduce_only=reduce_only,
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
)
return request.model_dump(exclude_none=True, by_alias=True, mode="json")

Expand Down Expand Up @@ -260,6 +263,7 @@ def create(
reduce_only: bool | None = ...,
subaccount: int | None = ...,
order_group_id: str | None = ...,
market_version: int | None = ...,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse: ...
def create(
Expand All @@ -279,12 +283,16 @@ def create(
reduce_only: bool | None = None,
subaccount: int | None = None,
order_group_id: str | None = None,
market_version: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse:
"""Place a new margin order (POST /margin/orders). Not retried.

``expiration_time`` is int64 Unix seconds per spec. ``subaccount``
is carried in the request *body* (0 = primary).
is carried in the request *body* (0 = primary). ``market_version``
is the expected market version; if set and the market's current
version differs, the server rejects with HTTP 409 /
``market_version_mismatch``. Omit to skip the check (server default 0).
"""
self._require_auth()
body = _build_create_body(
Expand All @@ -302,6 +310,7 @@ def create(
reduce_only=reduce_only,
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
)
data = self._post("/margin/orders", json=body, extra_headers=extra_headers)
return CreateMarginOrderResponse.model_validate(data)
Expand Down Expand Up @@ -621,6 +630,7 @@ async def create(
reduce_only: bool | None = ...,
subaccount: int | None = ...,
order_group_id: str | None = ...,
market_version: int | None = ...,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse: ...
async def create(
Expand All @@ -640,6 +650,7 @@ async def create(
reduce_only: bool | None = None,
subaccount: int | None = None,
order_group_id: str | None = None,
market_version: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse:
"""Place a new margin order. See :meth:`MarginOrdersResource.create`."""
Expand All @@ -659,6 +670,7 @@ async def create(
reduce_only=reduce_only,
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
)
data = await self._post("/margin/orders", json=body, extra_headers=extra_headers)
return CreateMarginOrderResponse.model_validate(data)
Expand Down
20 changes: 16 additions & 4 deletions kalshi/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@ def fills(
exchange_index: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> Page[Fill]:
"""List trade fills."""
"""List trade fills.

``ticker`` accepts a comma-separated list of up to 100 market tickers.
"""
self._require_auth()
params = _fills_params(
ticker=ticker,
Expand Down Expand Up @@ -225,7 +228,10 @@ def fills_all(
max_pages: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> Iterator[Fill]:
"""Auto-paginate trade fills."""
"""Auto-paginate trade fills.

``ticker`` accepts a comma-separated list of up to 100 market tickers.
"""
self._require_auth()
_validate_max_pages(max_pages)
params = _fills_params(
Expand Down Expand Up @@ -505,7 +511,10 @@ async def fills(
exchange_index: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> Page[Fill]:
"""List trade fills (async)."""
"""List trade fills (async).

``ticker`` accepts a comma-separated list of up to 100 market tickers.
"""
self._require_auth()
params = _fills_params(
ticker=ticker,
Expand Down Expand Up @@ -537,7 +546,10 @@ def fills_all(
max_pages: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> AsyncIterator[Fill]:
"""Auto-paginate trade fills (async). Use ``async for``."""
"""Auto-paginate trade fills (async). Use ``async for``.

``ticker`` accepts a comma-separated list of up to 100 market tickers.
"""
self._require_auth()
_validate_max_pages(max_pages)
params = _fills_params(
Expand Down
11 changes: 10 additions & 1 deletion kalshi/resources/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ def fills(
) -> Page[Fill]:
"""List trade fills (``GET /portfolio/fills``).

``ticker`` accepts a comma-separated list of up to 100 market tickers.

Moved from :class:`OrdersResource` in v3.0.0 (issue #351) to group
with the rest of the ``/portfolio/*`` family (``settlements``,
``deposits``, ``withdrawals``).
Expand Down Expand Up @@ -277,7 +279,10 @@ def fills_all(
max_pages: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> Iterator[Fill]:
"""Auto-paginate trade fills. Moved from :class:`OrdersResource` in v3.0.0."""
"""Auto-paginate trade fills. Moved from :class:`OrdersResource` in v3.0.0.

``ticker`` accepts a comma-separated list of up to 100 market tickers.
"""
self._require_auth()
_validate_max_pages(max_pages)
params = _fills_params(
Expand Down Expand Up @@ -656,6 +661,8 @@ async def fills(
) -> Page[Fill]:
"""List trade fills (``GET /portfolio/fills``, async).

``ticker`` accepts a comma-separated list of up to 100 market tickers.

Moved from :class:`AsyncOrdersResource` in v3.0.0 (issue #351).
"""
self._require_auth()
Expand Down Expand Up @@ -688,6 +695,8 @@ def fills_all(
) -> AsyncIterator[Fill]:
"""Auto-paginate trade fills (async). Use ``async for``.

``ticker`` accepts a comma-separated list of up to 100 market tickers.

Moved from :class:`AsyncOrdersResource` in v3.0.0 (issue #351).
"""
self._require_auth()
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 = "17.1.0"
version = "18.0.0"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
10 changes: 5 additions & 5 deletions specs/asyncapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2361,7 +2361,7 @@ components:
lastUpdateReason:
type: string
description: Order update reason. ReduceOnlyCancel means reduce_only capped the order at placement.
enum: ["Decrease", "Amend", "MarginCancel", "SelfTradeCancel", "ExpiryCancel", "CloseCancel", "HaltCancel", "Trade", "PostOnlyCrossCancel", "ReduceOnlyCancel"]
enum: ["Decrease", "Amend", "MarginCancel", "SelfTradeCancel", "ExpiryCancel", "CloseCancel", "HaltCancel", "Trade", "PostOnlyCrossCancel", "ReduceOnlyCancel", "SettlementBoundsCancel"]

orderAction:
type: string
Expand Down Expand Up @@ -3847,7 +3847,7 @@ components:
description: Unique identifier for the RFQ
creator_id:
type: string
description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled.
description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID.
market_ticker:
type: string
description: Market ticker for the RFQ
Expand Down Expand Up @@ -3911,7 +3911,7 @@ components:
description: Unique identifier for the RFQ
creator_id:
type: string
description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled.
description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID.
market_ticker:
type: string
description: Market ticker for the RFQ
Expand Down Expand Up @@ -3957,7 +3957,7 @@ components:
description: Public communications ID of the quote creator (anonymized)
rfq_creator_id:
type: string
description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled.
description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID.
market_ticker:
type: string
description: Market ticker for the quote
Expand Down Expand Up @@ -4017,7 +4017,7 @@ components:
description: Public communications ID of the quote creator (anonymized)
rfq_creator_id:
type: string
description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled.
description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID.
market_ticker:
type: string
description: Market ticker for the quote
Expand Down
13 changes: 9 additions & 4 deletions specs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1964,7 +1964,12 @@ paths:
kalshiAccessTimestamp: []
- kalshiOauthAccessToken: []
parameters:
- $ref: '#/components/parameters/TickerQuery'
- name: ticker
in: query
description: Filter by market ticker. Accepts a comma-separated list of up to 100 market tickers.
schema:
type: string
x-go-type-skip-optional-pointer: true
- $ref: '#/components/parameters/OrderIdQuery'
- $ref: '#/components/parameters/MinTsQuery'
- $ref: '#/components/parameters/MaxTsQuery'
Expand Down Expand Up @@ -7816,7 +7821,7 @@ components:
description: UUID of the RFQ. Preserve the exact returned string.
creator_id:
type: string
description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled.
description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. After successful quote execution, subsequent RFQ responses show the normal public communications ID to all viewers.
market_ticker:
type: string
description: The ticker of the market this RFQ is for
Expand Down Expand Up @@ -7936,7 +7941,7 @@ components:
x-go-type-skip-optional-pointer: true
obscure_creator_id:
type: boolean
description: Hide the RFQ creator ID from other users until successful execution. The creator always sees their own ID.
description: Replace the RFQ creator's public communications ID with a shared empty-input SHA-256 hash for other users. Applies to RFQ responses and broadcasts, and to quotes before successful execution. Successful execution reveals the normal ID in subsequent RFQ responses and in the executed quote. Accepting or confirming a quote does not reveal the ID. The creator always sees their normal ID. See the RFQ guide for the placeholder and visibility rules.
default: false
x-go-type-skip-optional-pointer: true
rest_remainder:
Expand Down Expand Up @@ -7990,7 +7995,7 @@ components:
description: Public communications ID of the quote creator
rfq_creator_id:
type: string
description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled.
description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. After this quote executes successfully, its counterparty sees the normal public communications ID.
x-go-type-skip-optional-pointer: true
market_ticker:
type: string
Expand Down
Loading
Loading