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

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

## 3.1.0 — 2026-06-03

OpenAPI + AsyncAPI spec sync from v3.19.0 → v3.20.0 (`#385`). Adds the
new `GET /events/fee_changes` endpoint plus three smaller additive
changes upstream re-published into 3.20.0 after the drift issue was
filed (the OpenAPI checksum in `#385` is therefore stale; the AsyncAPI
checksum still matches).

### Added

- `events.fee_changes()` / `fee_changes_all()` (sync + async) for
`GET /events/fee_changes` — the new paginated event-level fee-override
feed (`event_ticker`, `limit`, `cursor`). Returns `Page[EventFeeChange]`
(and an auto-paginating iterator). `EventFeeChange` exposes
`fee_type_override` / `fee_multiplier_override`, both present-but-`None`
when an override is cleared.
- `is_block_trade` query param on `markets.list_trades` /
`list_all_trades` (+ deprecated `list_trades_all`) and
`historical.trades` / `trades_all`. Omit for all trades, `True` for
only block trades, `False` for only non-block.
- `Trade.is_block_trade` (`bool`) — new spec-required, non-nullable
response field; a missing key raises so a schema regression surfaces.
- WebSocket `event_fee_update` message on the existing
`market_lifecycle_v2` channel (`EventFeeUpdateMessage` /
`EventFeeUpdatePayload`). `subscribe_market_lifecycle()` now yields
`MarketLifecycleMessage | EventFeeUpdateMessage`. Channel count is
unchanged (still 11) — this is a second message type on an existing
channel. **Behavioral note for existing subscribers:** discriminate on
`.type` before reading payload fields — an `EventFeeUpdatePayload` has no
`market_ticker`, so naive access raises `AttributeError`. See the
migration callout in [`docs/websockets.md`](docs/websockets.md).

### Internal

- `specs/openapi.yaml` (sha256
`b72a2aa138695d810f6ca85096bfe19e1b66ba5e9b2ed37753be284b5288d271`)
and `specs/asyncapi.yaml` (sha256
`2f72d0a3fd25fe331210ed300f03ad4c1fedcb561b3ab425046b2dca6f4683ec`)
snapshots bumped; `kalshi/_generated/models.py` regenerated.
- Spec also relaxed `CreateOrderV2Request.client_order_id` and
`EventData.product_metadata` from required to optional, and added
`ApiKeyScope` / `FeeType` enums. No SDK-facade change: the V2 order
keeps `client_order_id` required by design, `Event.product_metadata`
already tolerated server omission, and API-key `scopes` stays `str`
for forward-compat.
- README + `docs/index.md` banners bumped to "99 operations … OpenAPI
v3.20.0".

## 3.0.1 — 2026-05-26

OpenAPI spec sync from v3.18.0 → v3.19.0 (`#383`). Single additive
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.18.0, 98 operations)
- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.20.0, 99 operations)
- AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (13 WebSocket channels)
- 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 (98 operations across 19 resources, OpenAPI v3.19.0) and WebSocket API (11 typed `subscribe_*` channels + 2 escape-hatch).
- **Full coverage** of the Kalshi REST API (99 operations across 19 resources, OpenAPI v3.20.0) and WebSocket API (11 typed `subscribe_*` channels + 2 escape-hatch).
- **V2 event-market orders**: `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` plus batched variants on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` keeps working — deprecated no earlier than May 6, 2026.
- **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`.
- **Sync and async** clients sharing one transport — no thread-pool wrapping.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) prediction
markets API.

- **Full REST coverage** — 98 operations across 19 resources (OpenAPI v3.19.0),
- **Full REST coverage** — 99 operations across 19 resources (OpenAPI v3.20.0),
every kwarg drift-tested against the spec.
- **V2 event-market orders** — new `create_v2` / `amend_v2` / `decrease_v2` /
`cancel_v2` family on `/portfolio/events/orders/*`. Legacy `/portfolio/orders`
Expand Down
43 changes: 36 additions & 7 deletions docs/resources/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ more markets that share a resolution.
| `list_all_multivariate(...)` | walks `list_multivariate` | no |
| `get(event_ticker, *, with_nested_markets=False)` | `GET /events/{event_ticker}` | no |
| `metadata(event_ticker)` | `GET /events/{event_ticker}/metadata` | no |
| `fee_changes(...)` | `GET /events/fee_changes` | no |
| `fee_changes_all(...)` | walks `fee_changes` | no |

## List events

Expand Down Expand Up @@ -64,16 +66,43 @@ print(md.tags, md.category)
Two `EventMetadata`-adjacent fields are typed as nullable to absorb live
server behavior:

- `Event.product_metadata` is typed `dict[str, Any] | None`. The OpenAPI
spec marks it `required`, but the live demo server omits the key on
most events. Defaults to `None`.
- `Event.product_metadata` is typed `dict[str, Any] | None` and defaults
to `None`. The live demo server omits the key on most events. As of
OpenAPI v3.20.0 the spec also marks it optional, so this is no longer a
spec deviation — the SDK simply keeps it nullable to match reality.
- `EventMetadata.market_details: list[MarketMetadata]` — when the live
server sends JSON `null`, the SDK coerces it to `[]` so callers
always see a list. The spec contract (key present) is still
server sends JSON `null`, the SDK coerces it to `[]` (via `NullableList`)
so callers always see a list. The spec contract (key present) is still
enforced.

Both are tracked under `server_omits_despite_required` in the SDK's
EXCLUSIONS map.
## Event fee changes

`fee_changes()` returns the paginated feed of scheduled **event-level** fee
overrides (`GET /events/fee_changes`, added in v3.20.0). Event fees layer on
top of the parent series' fee structure.

```python
page = client.events.fee_changes(event_ticker="KXPRES-24", limit=100)
for change in page:
if change.fee_type_override is None:
print(change.event_ticker, "override cleared")
else:
print(change.event_ticker, change.fee_type_override, change.fee_multiplier_override)

# Or auto-paginate across every page:
for change in client.events.fee_changes_all(event_ticker="KXPRES-24"):
...
```

`EventFeeChange.fee_type_override` and `fee_multiplier_override` are both
`None` when an override has been **cleared** (the event falls back to the
series fee). Both keys are always present in the payload — `None` is a
meaningful "cleared" signal, not a missing field. The live `event_fee_update`
WebSocket message on the `market_lifecycle_v2` channel carries the same
override shape (see [WebSocket](../websockets.md)).

For the series-level equivalent, see
[`series.fee_changes`](series.md).

## Reference

Expand Down
1 change: 1 addition & 0 deletions docs/resources/historical.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ trades = client.historical.trades(
ticker="KXPRES-24-DJT",
min_ts=1_600_000_000,
max_ts=1_650_000_000,
is_block_trade=False, # v3.20.0: omit for all; True = only block, False = only non-block
limit=1000,
)
```
Expand Down
6 changes: 5 additions & 1 deletion docs/resources/markets.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,16 @@ page = client.markets.list_trades(
ticker="KXPRES-24-DJT",
min_ts=1_700_000_000,
max_ts=1_700_100_000,
is_block_trade=False, # omit for all trades; True = only block, False = only non-block
limit=200,
)
for trade in page:
print(trade.trade_id, trade.taker_side, trade.yes_price, trade.count)
print(trade.trade_id, trade.taker_side, trade.yes_price, trade.count, trade.is_block_trade)
```

`is_block_trade` (v3.20.0) filters by whether a trade is a block trade. Omit it
to return all trades. Each `Trade` also carries an `is_block_trade: bool`.

!!! warning "Deprecated since v3.0.0"
`list_trades_all` is the legacy name; it still works but emits
`DeprecationWarning` and will be removed in a future release. Use
Expand Down
26 changes: 25 additions & 1 deletion docs/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ SDK's perspective on it.
| `subscribe_ticker` | `ticker` | `ticker` | `TickerMessage` | public |
| `subscribe_trade` | `trade` | `trade` | `TradeMessage` | public |
| `subscribe_orderbook_delta` | `orderbook_delta` | `orderbook_snapshot` → `orderbook_delta` | `OrderbookSnapshotMessage` / `OrderbookDeltaMessage` | public |
| `subscribe_market_lifecycle` | `market_lifecycle_v2` | `market_lifecycle_v2` | `MarketLifecycleMessage` | public |
| `subscribe_market_lifecycle` | `market_lifecycle_v2` | `market_lifecycle_v2` / `event_fee_update` | `MarketLifecycleMessage` / `EventFeeUpdateMessage` | public |
| `subscribe_multivariate` | `multivariate` | `multivariate_lookup` | `MultivariateMessage` | public |
| `subscribe_multivariate_lifecycle` | `multivariate_market_lifecycle` | `multivariate_market_lifecycle` | `MultivariateLifecycleMessage` | public |
| `subscribe_fill` | `fill` | `fill` | `FillMessage` | private |
Expand All @@ -49,6 +49,30 @@ The `type` column matters when filtering raw logs — note the singular forms
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
`subscribe_market_lifecycle()` now yields
`MarketLifecycleMessage | EventFeeUpdateMessage`. **Existing consumers must
discriminate on `.type` before touching payload fields** — an
`EventFeeUpdatePayload` has no `market_ticker`, so naive access raises
`AttributeError`:

```python
async for msg in session.subscribe_market_lifecycle():
if msg.type == "event_fee_update":
print(msg.msg.event_ticker, msg.msg.fee_type_override) # None when cleared
else: # market_lifecycle_v2
print(msg.msg.market_ticker, msg.msg.event_type)
```

This is a second message **type** on the same channel — the channel count
stays 11. The override payload mirrors the REST
[`EventFeeChange`](resources/events.md#event-fee-changes):
`EventFeeUpdatePayload` carries `event_ticker`, `fee_type_override`, and
`fee_multiplier_override` (the latter two `None` when the override is
cleared).

Two channels carry monotonic `seq` numbers and have built-in sequence-gap
recovery: `orderbook_delta` (which delivers both snapshot and delta envelopes
under one subscription) and `order_group_updates`.
Expand Down
4 changes: 3 additions & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
EndpointTokenCost,
Event,
EventCandlesticks,
EventFeeChange,
EventMetadata,
EventPosition,
EventStatusLiteral,
Expand Down Expand Up @@ -236,6 +237,7 @@
"EndpointTokenCost",
"Event",
"EventCandlesticks",
"EventFeeChange",
"EventMetadata",
"EventPosition",
"EventStatusLiteral",
Expand Down Expand Up @@ -351,4 +353,4 @@
"Withdrawal",
]

__version__ = "3.0.1"
__version__ = "3.1.0"
9 changes: 9 additions & 0 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class ContractEntry:
spec_schema="EventData",
notes="Spec uses 'EventData', not 'Event'",
),
ContractEntry(
sdk_model="kalshi.models.events.EventFeeChange",
spec_schema="EventFeeChange",
),
ContractEntry(
sdk_model="kalshi.models.exchange.ExchangeStatus",
spec_schema="ExchangeStatus",
Expand Down Expand Up @@ -514,6 +518,11 @@ class ContractEntry:
notes="SDK conflates lifecycle + event fields. "
"Spec has additional_metadata, price_level_structure not in SDK.",
),
ContractEntry(
sdk_model="kalshi.ws.models.event_fee.EventFeeUpdatePayload",
spec_schema="eventFeeUpdatePayload",
notes="New v3.20.0 message (#385); rides the market_lifecycle_v2 channel.",
),
ContractEntry(
sdk_model="kalshi.ws.models.market_positions.MarketPositionsPayload",
spec_schema="marketPositionPayload",
Expand Down
2 changes: 2 additions & 0 deletions kalshi/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
)
from kalshi.models.events import (
Event,
EventFeeChange,
EventMetadata,
EventStatusLiteral,
MarketMetadata,
Expand Down Expand Up @@ -232,6 +233,7 @@
"EndpointTokenCost",
"Event",
"EventCandlesticks",
"EventFeeChange",
"EventMetadata",
"EventPosition",
"EventStatusLiteral",
Expand Down
28 changes: 25 additions & 3 deletions kalshi/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ class Event(BaseModel):
strike_date: AwareDatetime | None = None
strike_period: str | None = None
available_on_brokers: bool
# Spec marks `product_metadata` required, but the live demo server omits
# it on most events (observed run #26141405845, 2026-05-20). Recorded in
# tests/_contract_support.py EXCLUSIONS as `server_omits_despite_required`.
# The live demo server omits `product_metadata` on most events (observed
# run #26141405845, 2026-05-20). OpenAPI v3.20.0 relaxed it to optional
# too (#385), so this is no longer a spec deviation — kept nullable to
# match server reality.
product_metadata: dict[str, Any] | None = None
last_updated_ts: AwareDatetime | None = None
markets: NullableList[Market] = []
Expand All @@ -43,6 +44,27 @@ class Event(BaseModel):
model_config = {"extra": "allow", "populate_by_name": True}


class EventFeeChange(BaseModel):
"""A scheduled event-level fee override (GET /events/fee_changes).

Event fees layer on top of the parent series' fee structure.
``fee_type_override`` and ``fee_multiplier_override`` are both ``None``
when the override has been cleared (the event falls back to the series
fee). Both are spec-required keys (``EventFeeChange.required``) but
nullable — the key must be present; ``None`` is a meaningful "override
cleared" signal, so neither carries a default.
"""

id: str
event_ticker: str
series_ticker: str
fee_type_override: str | None
fee_multiplier_override: MultiplierDecimal | None
scheduled_ts: AwareDatetime

model_config = {"extra": "allow"}


class MarketMetadata(BaseModel):
"""Metadata for a market within an event."""

Expand Down
5 changes: 5 additions & 0 deletions kalshi/models/historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,9 @@ class Trade(BaseModel):
taker_outcome_side: SideLiteral
taker_book_side: BookSideLiteral

# v3.20.0 (#385). Spec marks `is_block_trade` required + non-nullable
# (Trade.required). Typed strict `bool` like taker_outcome_side: a missing
# key raises ValidationError so a schema regression surfaces, not a default.
is_block_trade: bool

model_config = {"extra": "allow", "populate_by_name": True}
6 changes: 4 additions & 2 deletions kalshi/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,10 @@ class CreateOrderV2Request(BaseModel):
- ``side`` is ``BookSideLiteral`` (``bid``/``ask``), not ``yes``/``no``.
V2 narrows the type on the model itself since there is no kwarg
overload at the resource-method boundary (see model_only V2 surface).
- ``client_order_id`` is **required** in V2, unlike V1 where it is
optional. The server uses it for idempotency in V2.
- ``client_order_id`` is **required** by this model, unlike V1 where it is
optional, because the server uses it for idempotency on V2 orders.
(OpenAPI v3.20.0 relaxed it to optional server-side, but the SDK keeps
it required by design so every V2 order is idempotent.)
- Price is a single ``price: DollarDecimal`` field rather than the
paired ``yes_price`` / ``no_price`` from V1.
"""
Expand Down
Loading