From c9b77f3f65d27b2f43efa6a1f256edaa78fe3d0e Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sat, 27 Jun 2026 10:06:55 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20ExchangeStatus=20spec=20drift=20?= =?UTF-8?q?=E2=80=94=20add=20intra=5Fexchange=5Ftransfers=5Factive=20+=20e?= =?UTF-8?q?xchange=5Findex=5Fstatuses=20(#460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kalshi added two 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. - Re-sync specs/openapi.yaml (38-line additive diff in the exchange region) - ExchangeStatus: add optional intra_exchange_transfers_active and exchange_index_statuses (NullableList, defaults to []) - New ExchangeIndexStatus model (per-index/shard status); register in _contract_map.py (required by test_contract_map_completeness) and export from kalshi + kalshi.models - Regression tests: per-index breakdown, optional-field defaults, null->[] - Bump version 5.0.0 -> 5.0.1 + CHANGELOG Closes #460 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 22 ++++++++++ kalshi/__init__.py | 4 +- kalshi/_contract_map.py | 5 +++ kalshi/models/__init__.py | 2 + kalshi/models/exchange.py | 13 ++++++ pyproject.toml | 2 +- specs/openapi.yaml | 38 +++++++++++++++++ tests/test_exchange.py | 87 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 171 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99272d43..82897ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/kalshi/__init__.py b/kalshi/__init__.py index f24ee85b..80fc6800 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -77,6 +77,7 @@ EventMetadata, EventPosition, EventStatusLiteral, + ExchangeIndexStatus, ExchangeStatus, Fill, ForecastPercentilesPoint, @@ -247,6 +248,7 @@ "EventMetadata", "EventPosition", "EventStatusLiteral", + "ExchangeIndexStatus", "ExchangeStatus", "Fill", "FixClient", @@ -373,4 +375,4 @@ "Withdrawal", ] -__version__ = "5.0.0" +__version__ = "5.0.1" diff --git a/kalshi/_contract_map.py b/kalshi/_contract_map.py index 43c60d35..f990e546 100644 --- a/kalshi/_contract_map.py +++ b/kalshi/_contract_map.py @@ -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", diff --git a/kalshi/models/__init__.py b/kalshi/models/__init__.py index 48a5cfb3..70915443 100644 --- a/kalshi/models/__init__.py +++ b/kalshi/models/__init__.py @@ -53,6 +53,7 @@ from kalshi.models.exchange import ( Announcement, DailySchedule, + ExchangeIndexStatus, ExchangeStatus, MaintenanceWindow, Schedule, @@ -228,6 +229,7 @@ "EventMetadata", "EventPosition", "EventStatusLiteral", + "ExchangeIndexStatus", "ExchangeStatus", "Fill", "ForecastPercentilesPoint", diff --git a/kalshi/models/exchange.py b/kalshi/models/exchange.py index 5e73853c..f58bb8cd 100644 --- a/kalshi/models/exchange.py +++ b/kalshi/models/exchange.py @@ -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"} diff --git a/pyproject.toml b/pyproject.toml index ba08e6d2..b6fd4219 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 37934a84..0f9075c2 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -4872,6 +4872,11 @@ 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 @@ -4879,6 +4884,39 @@ components: 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: diff --git a/tests/test_exchange.py b/tests/test_exchange.py index a7a6f495..f40a4730 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -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 @@ -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