diff --git a/CHANGELOG.md b/CHANGELOG.md index 99272d4..82897ba 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 f24ee85..80fc680 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 43c60d3..f990e54 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 48a5cfb..7091544 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 5e73853..f58bb8c 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 ba08e6d..b6fd421 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 37934a8..0f9075c 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 a7a6f49..f40a473 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