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

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

## 5.0.1 — 2026-06-27

Spec-drift catch-up (#460). Kalshi added fields to the `ExchangeStatus` schema
in place — without bumping the OpenAPI `version` (still 3.22.0) — so the
nightly strict contract suite flagged additive drift the day after the 5.0.0
sync. All changes are additive and backward-compatible.

### Fixed

- **`ExchangeStatus` additive drift.** Added two optional fields that upstream
introduced on `GET /exchange/status`:
- `intra_exchange_transfers_active: bool | None` — whether intra-exchange
transfers are currently permitted (omitted by older servers, hence optional).
- `exchange_index_statuses: list[ExchangeIndexStatus]` — per-index (shard)
status breakdown; defaults to `[]` when absent or null.

### Added

- **`ExchangeIndexStatus`** model (exported from `kalshi` and `kalshi.models`):
per-exchange-index operational status with `exchange_index`, `exchange_active`,
`trading_active`, and `intra_exchange_transfers_active`.

## 5.0.0 — 2026-06-26

Syncs the upstream OpenAPI spec **3.21.0 → 3.22.0** (#454, #458). The headline
Expand Down
4 changes: 3 additions & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
EventMetadata,
EventPosition,
EventStatusLiteral,
ExchangeIndexStatus,
ExchangeStatus,
Fill,
ForecastPercentilesPoint,
Expand Down Expand Up @@ -247,6 +248,7 @@
"EventMetadata",
"EventPosition",
"EventStatusLiteral",
"ExchangeIndexStatus",
"ExchangeStatus",
"Fill",
"FixClient",
Expand Down Expand Up @@ -373,4 +375,4 @@
"Withdrawal",
]

__version__ = "5.0.0"
__version__ = "5.0.1"
5 changes: 5 additions & 0 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,11 @@ class ContractEntry:
spec_schema="SettlementSource",
),
# -- exchange sub-models (#171) --------------------------------------
ContractEntry(
sdk_model="kalshi.models.exchange.ExchangeIndexStatus",
spec_schema="ExchangeIndexStatus",
notes="Per-index status on ExchangeStatus.exchange_index_statuses (#460)",
),
ContractEntry(
sdk_model="kalshi.models.exchange.Schedule",
spec_schema="Schedule",
Expand Down
2 changes: 2 additions & 0 deletions kalshi/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from kalshi.models.exchange import (
Announcement,
DailySchedule,
ExchangeIndexStatus,
ExchangeStatus,
MaintenanceWindow,
Schedule,
Expand Down Expand Up @@ -228,6 +229,7 @@
"EventMetadata",
"EventPosition",
"EventStatusLiteral",
"ExchangeIndexStatus",
"ExchangeStatus",
"Fill",
"ForecastPercentilesPoint",
Expand Down
13 changes: 13 additions & 0 deletions kalshi/models/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@
from kalshi.types import NullableList


class ExchangeIndexStatus(BaseModel):
"""Operational status of a single exchange index (shard)."""

exchange_index: int
exchange_active: bool
trading_active: bool
intra_exchange_transfers_active: bool

model_config = {"extra": "allow"}


class ExchangeStatus(BaseModel):
"""Current exchange operational status."""

exchange_active: bool
trading_active: bool
intra_exchange_transfers_active: bool | None = None
exchange_estimated_resume_time: AwareDatetime | None = None
exchange_index_statuses: NullableList[ExchangeIndexStatus] = []

model_config = {"extra": "allow"}

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 = "5.0.0"
version = "5.0.1"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
38 changes: 38 additions & 0 deletions specs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4872,13 +4872,51 @@ components:
True if we are currently permitting trading on the exchange. This is
true during trading hours and false outside exchange hours. Kalshi
reserves the right to pause at any time in case issues are detected.
intra_exchange_transfers_active:
type: boolean
description: >-
True if intra-exchange transfers are currently permitted. False when
transfers are temporarily blocked.
exchange_estimated_resume_time:
type: string
format: date-time
description: >-
Estimated downtime for the current exchange maintenance window.
However, this is not guaranteed and can be extended.
nullable: true
exchange_index_statuses:
type: array
description: >-
Status of each exchange index. The top-level fields above reflect
the default exchange index (0). Absent when the per-index breakdown
is unavailable.
items:
$ref: '#/components/schemas/ExchangeIndexStatus'
ExchangeIndexStatus:
type: object
required:
- exchange_index
- exchange_active
- trading_active
- intra_exchange_transfers_active
properties:
exchange_index:
$ref: '#/components/schemas/ExchangeIndex'
exchange_active:
type: boolean
description: >-
False if this exchange index is no longer taking any state changes
at all. True unless under maintenance.
trading_active:
type: boolean
description: >-
True if trading is currently permitted on this exchange index. False
outside exchange hours or during pauses.
intra_exchange_transfers_active:
type: boolean
description: >-
True if intra-exchange transfers are currently permitted on this
exchange index. False when transfers are temporarily blocked.
GetExchangeAnnouncementsResponse:
type: object
required:
Expand Down
87 changes: 87 additions & 0 deletions tests/test_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,65 @@ def test_null_resume_time(self, exchange: ExchangeResource) -> None:
assert status.exchange_active is False
assert status.exchange_estimated_resume_time is None

@respx.mock
def test_defaults_when_new_fields_absent(self, exchange: ExchangeResource) -> None:
"""intra_exchange_transfers_active and exchange_index_statuses are optional."""
respx.get("https://test.kalshi.com/trade-api/v2/exchange/status").mock(
return_value=httpx.Response(
200,
json={"exchange_active": True, "trading_active": True},
)
)
status = exchange.status()
assert status.intra_exchange_transfers_active is None
assert status.exchange_index_statuses == []

@respx.mock
def test_per_index_breakdown(self, exchange: ExchangeResource) -> None:
respx.get("https://test.kalshi.com/trade-api/v2/exchange/status").mock(
return_value=httpx.Response(
200,
json={
"exchange_active": True,
"trading_active": True,
"intra_exchange_transfers_active": False,
"exchange_index_statuses": [
{
"exchange_index": 0,
"exchange_active": True,
"trading_active": True,
"intra_exchange_transfers_active": False,
}
],
},
)
)
status = exchange.status()
assert status.intra_exchange_transfers_active is False
assert len(status.exchange_index_statuses) == 1
index_status = status.exchange_index_statuses[0]
assert index_status.exchange_index == 0
assert index_status.exchange_active is True
assert index_status.trading_active is True
assert index_status.intra_exchange_transfers_active is False

@respx.mock
def test_null_index_statuses_coerced_to_empty(
self, exchange: ExchangeResource
) -> None:
respx.get("https://test.kalshi.com/trade-api/v2/exchange/status").mock(
return_value=httpx.Response(
200,
json={
"exchange_active": True,
"trading_active": True,
"exchange_index_statuses": None,
},
)
)
status = exchange.status()
assert status.exchange_index_statuses == []


class TestExchangeSchedule:
@respx.mock
Expand Down Expand Up @@ -228,6 +287,34 @@ async def test_maintenance_mode(
assert status.trading_active is False
assert status.exchange_estimated_resume_time is not None

@respx.mock
@pytest.mark.asyncio
async def test_per_index_breakdown(
self, async_exchange: AsyncExchangeResource
) -> None:
respx.get("https://test.kalshi.com/trade-api/v2/exchange/status").mock(
return_value=httpx.Response(
200,
json={
"exchange_active": True,
"trading_active": True,
"intra_exchange_transfers_active": True,
"exchange_index_statuses": [
{
"exchange_index": 0,
"exchange_active": True,
"trading_active": True,
"intra_exchange_transfers_active": True,
}
],
},
)
)
status = await async_exchange.status()
assert status.intra_exchange_transfers_active is True
assert len(status.exchange_index_statuses) == 1
assert status.exchange_index_statuses[0].exchange_index == 0


class TestAsyncExchangeSchedule:
@respx.mock
Expand Down
Loading