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

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

## 19.0.0 — 2026-10-06

Reconciles upstream OpenAPI **3.32.0** content drift plus perps/AsyncAPI
updates after nightly contract failures (Closes #529). **Breaking** for
constructors of `MultivariateEventCollection` that omit the new required
`price_level_structure` and `price_ranges`. Re-vendored `specs/openapi.yaml` (hash
`f93ce4d0bff2c0db4de1b0eb6194f8d315b41d9ce336c2d5ed8ef3254fd77dfe`),
`specs/asyncapi.yaml` (hash
`eaf754def77f36cd016f1e852a5ce76f11b635140e879bee7a3f9cad40716f9f`), and
`specs/perps_openapi.yaml` (hash
`7edea6f0db672a1e8110cb699ff3cfd84af85d926f08610e4a6a73d51607e37e`).

### Changed (breaking)

- **`MultivariateEventCollection.price_level_structure`** (required `str`) and
**`MultivariateEventCollection.price_ranges`** (required list of
`{start, end, step}` dicts; JSON `null` coerces to `[]`) — pricing used for
new markets created in the collection. Same wire shape as
`Market.price_ranges`. Live list/get callers are unaffected; tests/mocks that
construct `MultivariateEventCollection` must pass both.

### Added

- Optional **`market_id`** (stable exchange market UUID) on perps
`CreateMarginOrderRequest` / `AmendMarginOrderRequest` and the
`orders.create` / `orders.amend` kwargs (sync + async). Ticker takes
precedence when both are set. Omitted from the body when unset.

### Spec notes

- Core OpenAPI `info.version` still **3.32.0**.
- Perps spec now accepts `market_id` as an alternative to `ticker` on create
and amend; the SDK keeps `ticker` required for now.
- AsyncAPI adds the `Metal.Index.1OZGOLD/USD` (3712) price index to the docs table.

## 18.0.0 — 2026-10-03

Reconciles upstream OpenAPI **3.32.0** content drift plus perps/AsyncAPI
Expand Down
43 changes: 43 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
# Migration

## v18.0 → v19.0.0

Reconciles upstream OpenAPI **3.32.0** content drift plus matching perps
updates (Closes #529). **Breaking** for constructors of
`MultivariateEventCollection` that omit the new required
`price_level_structure` and `price_ranges`.

### Response model field changes

- **`MultivariateEventCollection.price_level_structure`** — required `str`.
Price level structure used for new markets created in this collection.
Existing markets may differ; use the minted `Market`'s
`price_level_structure` / `price_ranges` for its current pricing.
- **`MultivariateEventCollection.price_ranges`** — required list of
`{start, end, step}` dicts (empty list allowed; JSON `null` coerces to
`[]`). Price bands and tick sizes in fixed-point dollars for new markets
created in this collection. Same wire shape as `Market.price_ranges`.

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

```python
# Before (constructors / test fixtures):
# MultivariateEventCollection(..., functional_description="")

# After:
MultivariateEventCollection(
...,
functional_description="",
price_level_structure="binary",
price_ranges=[],
)
```

### Added (non-breaking)

- Optional `CreateMarginOrderRequest.market_id` /
`AmendMarginOrderRequest.market_id` and
`perps.orders.create(..., market_id=)` /
`perps.orders.amend(..., market_id=)` (sync + async). Stable exchange
market UUID; may be supplied with `ticker` (ticker takes precedence when
both are set). Omit or leave unset so the key is not sent.

## v17.1 → v18.0.0

Reconciles upstream OpenAPI **3.32.0** content drift plus matching perps and
Expand Down
3 changes: 3 additions & 0 deletions docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ the current expiry, or pass `0` to clear it (good-till-canceled).
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.
`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.

!!! 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
7 changes: 6 additions & 1 deletion docs/resources/multivariate.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ page = client.multivariate_collections.list(
limit=100,
)
for c in page:
print(c.collection_ticker, c.title, c.exchange_index)
print(c.collection_ticker, c.title, c.price_level_structure, c.exchange_index)
```

Each collection includes required `price_level_structure` (`str`) and
`price_ranges` (list of `{start, end, step}` dicts) describing the price
bands used for **new** markets created in the collection. Existing markets
may differ — use the minted `Market`'s fields for its current pricing.

## Select legs

A leg is a `TickerPair(event_ticker=..., market_ticker=...)` — one event-side
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__ = "18.0.0"
__version__ = "19.0.0"
9 changes: 8 additions & 1 deletion kalshi/models/multivariate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import Literal
from typing import Any, Literal

from pydantic import AwareDatetime, BaseModel

Expand Down Expand Up @@ -51,6 +51,13 @@ class MultivariateEventCollection(BaseModel):
size_min: int
size_max: int
functional_description: str
# Spec-required. Used for new markets created in this collection;
# existing markets may differ — use Market.price_level_structure /
# Market.price_ranges for a minted market's current pricing.
# price_ranges is list[PriceRange] on the wire — kept as list[dict]
# here to match Market.price_ranges (no nested model yet).
price_level_structure: str
price_ranges: NullableList[dict[str, Any]]
# Optional: exchange shard inherited from the collection's series.
exchange_index: int | None = None

Expand Down
8 changes: 8 additions & 0 deletions kalshi/perps/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ class CreateMarginOrderRequest(BaseModel):
# 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
# Stable exchange market UUID. May be supplied instead of ticker;
# ticker takes precedence when both are provided. Omit (None) so
# exclude_none drops the key.
market_id: str | None = None


class DecreaseMarginOrderRequest(BaseModel):
Expand Down Expand Up @@ -143,6 +147,10 @@ class AmendMarginOrderRequest(BaseModel):
updated_client_order_id: str | None = None
# Unix seconds. Omit preserves the current expiry; 0 removes it (GTC).
expiration_time: StrictInt | None = Field(default=None, ge=0)
# Stable exchange market UUID. May be supplied instead of ticker;
# ticker takes precedence when both are provided. Omit (None) so
# exclude_none drops the key.
market_id: str | None = None


# ── Response models (extra="allow") ──────────────────────────────────────────
Expand Down
25 changes: 24 additions & 1 deletion kalshi/perps/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def _build_create_body(
subaccount: int | None,
order_group_id: str | None,
market_version: int | None,
market_id: str | None,
) -> dict[str, Any]:
_check_request_exclusive(
request,
Expand All @@ -104,6 +105,7 @@ def _build_create_body(
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
market_id=market_id,
)
if request is None:
if (
Expand Down Expand Up @@ -135,6 +137,7 @@ def _build_create_body(
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
market_id=market_id,
)
return request.model_dump(exclude_none=True, by_alias=True, mode="json")

Expand Down Expand Up @@ -166,6 +169,7 @@ def _build_amend_body(
client_order_id: str | None,
updated_client_order_id: str | None,
expiration_time: int | None,
market_id: str | None,
) -> dict[str, Any]:
_check_request_exclusive(
request,
Expand All @@ -176,6 +180,7 @@ def _build_amend_body(
client_order_id=client_order_id,
updated_client_order_id=updated_client_order_id,
expiration_time=expiration_time,
market_id=market_id,
)
if request is None:
if ticker is None or side is None or price is None or count is None:
Expand All @@ -191,6 +196,7 @@ def _build_amend_body(
client_order_id=client_order_id,
updated_client_order_id=updated_client_order_id,
expiration_time=expiration_time,
market_id=market_id,
)
return request.model_dump(exclude_none=True, by_alias=True, mode="json")

Expand Down Expand Up @@ -264,6 +270,7 @@ def create(
subaccount: int | None = ...,
order_group_id: str | None = ...,
market_version: int | None = ...,
market_id: str | None = ...,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse: ...
def create(
Expand All @@ -284,6 +291,7 @@ def create(
subaccount: int | None = None,
order_group_id: str | None = None,
market_version: int | None = None,
market_id: str | None = None,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse:
"""Place a new margin order (POST /margin/orders). Not retried.
Expand All @@ -293,6 +301,9 @@ def create(
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).
``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.
"""
self._require_auth()
body = _build_create_body(
Expand All @@ -311,6 +322,7 @@ def create(
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
market_id=market_id,
)
data = self._post("/margin/orders", json=body, extra_headers=extra_headers)
return CreateMarginOrderResponse.model_validate(data)
Expand Down Expand Up @@ -485,6 +497,7 @@ def amend(
client_order_id: str | None = ...,
updated_client_order_id: str | None = ...,
expiration_time: int | None = ...,
market_id: str | None = ...,
subaccount: int | None = ...,
extra_headers: dict[str, str] | None = None,
) -> AmendMarginOrderResponse: ...
Expand All @@ -500,6 +513,7 @@ def amend(
client_order_id: str | None = None,
updated_client_order_id: str | None = None,
expiration_time: int | None = None,
market_id: str | None = None,
subaccount: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> AmendMarginOrderResponse:
Expand All @@ -510,7 +524,9 @@ def amend(

``expiration_time`` is int64 Unix seconds. Omit it to preserve the
current expiry; ``0`` removes the expiry (good-till-canceled). A
nonzero value must be in the future.
nonzero value must be in the future. ``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.
"""
self._require_auth()
body = _build_amend_body(
Expand All @@ -522,6 +538,7 @@ def amend(
client_order_id=client_order_id,
updated_client_order_id=updated_client_order_id,
expiration_time=expiration_time,
market_id=market_id,
)
params = _params(subaccount=subaccount)
data = self._post(
Expand Down Expand Up @@ -631,6 +648,7 @@ async def create(
subaccount: int | None = ...,
order_group_id: str | None = ...,
market_version: int | None = ...,
market_id: str | None = ...,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse: ...
async def create(
Expand All @@ -651,6 +669,7 @@ async def create(
subaccount: int | None = None,
order_group_id: str | None = None,
market_version: int | None = None,
market_id: str | None = None,
extra_headers: dict[str, str] | None = None,
) -> CreateMarginOrderResponse:
"""Place a new margin order. See :meth:`MarginOrdersResource.create`."""
Expand All @@ -671,6 +690,7 @@ async def create(
subaccount=subaccount,
order_group_id=order_group_id,
market_version=market_version,
market_id=market_id,
)
data = await self._post("/margin/orders", json=body, extra_headers=extra_headers)
return CreateMarginOrderResponse.model_validate(data)
Expand Down Expand Up @@ -836,6 +856,7 @@ async def amend(
client_order_id: str | None = ...,
updated_client_order_id: str | None = ...,
expiration_time: int | None = ...,
market_id: str | None = ...,
subaccount: int | None = ...,
extra_headers: dict[str, str] | None = None,
) -> AmendMarginOrderResponse: ...
Expand All @@ -851,6 +872,7 @@ async def amend(
client_order_id: str | None = None,
updated_client_order_id: str | None = None,
expiration_time: int | None = None,
market_id: str | None = None,
subaccount: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> AmendMarginOrderResponse:
Expand All @@ -865,6 +887,7 @@ async def amend(
client_order_id=client_order_id,
updated_client_order_id=updated_client_order_id,
expiration_time=expiration_time,
market_id=market_id,
)
params = _params(subaccount=subaccount)
data = await self._post(
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 = "18.0.0"
version = "19.0.0"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
1 change: 1 addition & 0 deletions specs/asyncapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ channels:
| 3086 | `Commodities.SOX6/USD` | Soybeans futures (Nov 2026) in USD |
| 3063 | `Commodities.Index.PYTHOIL/USD` | Blended Oil Index in USD |
| 3153 | `Metal.Index.GOLD/USD` | Blended Gold Index in USD |
| 3712 | `Metal.Index.1OZGOLD/USD` | 1-Ounce Gold Index in USD |
| 3154 | `Metal.Index.SILVER/USD` | Blended Silver Index in USD |
| 3045 | `Commodities.BRENTX6/USD` | Brent Crude futures (Nov 2026) in USD |
| 3525 | `Commodities.Index.CU/USD` | Copper Index in USD |
Expand Down
10 changes: 10 additions & 0 deletions specs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8981,6 +8981,8 @@ components:
- size_min
- size_max
- functional_description
- price_level_structure
- price_ranges
properties:
collection_ticker:
type: string
Expand Down Expand Up @@ -9038,6 +9040,14 @@ components:
functional_description:
type: string
description: A functional description of the collection describing how inputs affect the output.
price_level_structure:
type: string
description: Price level structure used for new markets created in this collection. Existing markets may differ; use the market's price_level_structure and price_ranges for its current pricing.
price_ranges:
type: array
description: Price bands and tick sizes in fixed-point dollars for new markets created in this collection. Use these ranges to determine valid prices rather than hardcoding the structure name.
items:
$ref: '#/components/schemas/PriceRange'

GetMultivariateEventCollectionResponse:
type: object
Expand Down
Loading
Loading