From 8800527f27f686d924e74d2f493f1bfbf56794d6 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Tue, 6 Oct 2026 09:43:55 -0500 Subject: [PATCH] Reconcile MVE price ranges + perps market_id drift (v19.0.0) Closes #529. Adds required MultivariateEventCollection.price_level_structure and price_ranges, and optional perps market_id on create/amend order requests and kwargs (sync + async), after nightly strict contract failures. Re-vendors OpenAPI/AsyncAPI/perps specs (core info.version remains 3.32.0). --- CHANGELOG.md | 35 +++++++++++ docs/migration.md | 43 +++++++++++++ docs/perps.md | 3 + docs/resources/multivariate.md | 7 ++- kalshi/__init__.py | 2 +- kalshi/models/multivariate.py | 9 ++- kalshi/perps/models/orders.py | 8 +++ kalshi/perps/resources/orders.py | 25 +++++++- pyproject.toml | 2 +- specs/asyncapi.yaml | 1 + specs/openapi.yaml | 10 +++ specs/perps_openapi.yaml | 23 +++++-- tests/_model_fixtures.py | 2 + tests/perps/test_orders.py | 100 +++++++++++++++++++++++++++++- tests/test_multivariate_models.py | 37 +++++++++++ 15 files changed, 294 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fdcca5..18c7408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/migration.md b/docs/migration.md index f148712..771df5d 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -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 diff --git a/docs/perps.md b/docs/perps.md index 0a8dcf2..883658d 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -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 diff --git a/docs/resources/multivariate.md b/docs/resources/multivariate.md index f0bf743..3ff1a65 100644 --- a/docs/resources/multivariate.md +++ b/docs/resources/multivariate.md @@ -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 diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 2857383..0e99178 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -427,4 +427,4 @@ "Withdrawal", ] -__version__ = "18.0.0" +__version__ = "19.0.0" diff --git a/kalshi/models/multivariate.py b/kalshi/models/multivariate.py index 8722f63..600c629 100644 --- a/kalshi/models/multivariate.py +++ b/kalshi/models/multivariate.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from pydantic import AwareDatetime, BaseModel @@ -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 diff --git a/kalshi/perps/models/orders.py b/kalshi/perps/models/orders.py index 1fd4ba0..c2d7f55 100644 --- a/kalshi/perps/models/orders.py +++ b/kalshi/perps/models/orders.py @@ -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): @@ -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") ────────────────────────────────────────── diff --git a/kalshi/perps/resources/orders.py b/kalshi/perps/resources/orders.py index ac96222..85a0e56 100644 --- a/kalshi/perps/resources/orders.py +++ b/kalshi/perps/resources/orders.py @@ -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, @@ -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 ( @@ -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") @@ -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, @@ -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: @@ -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") @@ -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( @@ -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. @@ -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( @@ -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) @@ -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: ... @@ -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: @@ -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( @@ -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( @@ -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( @@ -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`.""" @@ -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) @@ -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: ... @@ -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: @@ -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( diff --git a/pyproject.toml b/pyproject.toml index 8f69548..33fff9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/specs/asyncapi.yaml b/specs/asyncapi.yaml index 6b8ea1a..f960eaa 100644 --- a/specs/asyncapi.yaml +++ b/specs/asyncapi.yaml @@ -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 | diff --git a/specs/openapi.yaml b/specs/openapi.yaml index eb7a346..9a5fc84 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -8981,6 +8981,8 @@ components: - size_min - size_max - functional_description + - price_level_structure + - price_ranges properties: collection_ticker: type: string @@ -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 diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index 6d998f2..52eb085 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -2577,7 +2577,6 @@ components: CreateMarginOrderRequest: type: object required: - - ticker - client_order_id - side - count @@ -2587,8 +2586,16 @@ components: properties: ticker: type: string + description: Market ticker. Supply ticker or market_id; ticker takes precedence when both are provided. + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: required_without=MarketId + market_id: + type: string + description: Stable exchange market UUID. May be supplied instead of ticker. + x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: - validate: required,min=1 + validate: omitempty,uuid client_order_id: type: string x-go-type-skip-optional-pointer: true @@ -2798,16 +2805,22 @@ components: AmendMarginOrderRequest: type: object required: - - ticker - side - price - count properties: ticker: type: string - description: Market ticker + description: Market ticker. Supply ticker or market_id; ticker takes precedence when both are provided. + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: required_without=MarketId + market_id: + type: string + description: Stable exchange market UUID. May be supplied instead of ticker. + x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: - validate: required,min=1 + validate: omitempty,uuid side: $ref: '#/components/schemas/BookSide' description: Side of the order diff --git a/tests/_model_fixtures.py b/tests/_model_fixtures.py index b8ad02e..d5c0d88 100644 --- a/tests/_model_fixtures.py +++ b/tests/_model_fixtures.py @@ -267,6 +267,8 @@ def multivariate_event_collection_dict(**overrides: Any) -> dict[str, Any]: "size_min": 0, "size_max": 0, "functional_description": "", + "price_level_structure": "binary", + "price_ranges": [], } base.update(overrides) return base diff --git a/tests/perps/test_orders.py b/tests/perps/test_orders.py index 5cacca6..71f5184 100644 --- a/tests/perps/test_orders.py +++ b/tests/perps/test_orders.py @@ -100,6 +100,8 @@ def test_happy(self, perps_client: PerpsClient) -> None: assert "count_fp" not in body # unset optional market_version is omitted (exclude_none), not sent as 0 assert "market_version" not in body + # unset optional market_id is omitted (exclude_none) + assert "market_id" not in body @respx.mock def test_market_version_serialized(self, perps_client: PerpsClient) -> None: @@ -122,6 +124,54 @@ def test_market_version_serialized(self, perps_client: PerpsClient) -> None: body = json.loads(route.calls[0].request.content) assert body["market_version"] == 1 + @respx.mock + def test_market_id_serialized(self, perps_client: PerpsClient) -> None: + route = respx.post(f"{BASE}/margin/orders").mock( + return_value=httpx.Response( + 201, + json={"order_id": "ord-9", "fill_count": "0.00", "remaining_count": "100.00"}, + ) + ) + perps_client.orders.create( + ticker="BTC-PERP", + client_order_id="cid-9", + side="bid", + count="100", + price="0.56", + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + market_id="11111111-1111-1111-1111-111111111111", + ) + body = json.loads(route.calls[0].request.content) + assert body["market_id"] == "11111111-1111-1111-1111-111111111111" + assert body["ticker"] == "BTC-PERP" + + def test_market_id_omitted_from_dump_when_none(self) -> None: + req = CreateMarginOrderRequest( + ticker="BTC-PERP", + client_order_id="cid-9", + side="bid", + count="100", + price="0.56", + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert "market_id" not in body + + def test_market_id_rejects_non_string(self) -> None: + with pytest.raises(ValidationError): + CreateMarginOrderRequest( + ticker="BTC-PERP", + client_order_id="cid-9", + side="bid", + count="100", + price="0.56", + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + market_id={"not": "a-uuid"}, # type: ignore[arg-type] + ) + @respx.mock def test_conflict_maps(self, perps_client: PerpsClient) -> None: respx.post(f"{BASE}/margin/orders").mock( @@ -236,7 +286,7 @@ def test_request_model_path(self, perps_client: PerpsClient) -> None: @respx.mock async def test_async_happy(self, async_perps_client: AsyncPerpsClient) -> None: - respx.post(f"{BASE}/margin/orders").mock( + route = respx.post(f"{BASE}/margin/orders").mock( return_value=httpx.Response( 201, json={"order_id": "ord-9", "fill_count": "0.00", "remaining_count": "100.00"}, @@ -250,8 +300,11 @@ async def test_async_happy(self, async_perps_client: AsyncPerpsClient) -> None: price="0.56", time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross", + market_id="11111111-1111-1111-1111-111111111111", ) assert resp.remaining_count == Decimal("100.00") + body = json.loads(route.calls[0].request.content) + assert body["market_id"] == "11111111-1111-1111-1111-111111111111" await async_perps_client.close() @@ -537,6 +590,7 @@ def test_happy_nullable_fields_absent(self, perps_client: PerpsClient) -> None: assert body["price"] == "0.57" assert body["count"] == "80" assert body["side"] == "bid" + assert "market_id" not in body @respx.mock def test_happy_with_fills(self, perps_client: PerpsClient) -> None: @@ -573,6 +627,40 @@ def test_expiration_time_zero_is_sent(self, perps_client: PerpsClient) -> None: body = json.loads(route.calls[0].request.content) assert body["expiration_time"] == 0 + @respx.mock + def test_market_id_serialized(self, perps_client: PerpsClient) -> None: + route = respx.post(f"{BASE}/margin/orders/ord-1/amend").mock( + return_value=httpx.Response(200, json={"order_id": "ord-1"}) + ) + perps_client.orders.amend( + "ord-1", + ticker="BTC-PERP", + side="bid", + price="0.57", + count="80", + market_id="11111111-1111-1111-1111-111111111111", + ) + body = json.loads(route.calls[0].request.content) + assert body["market_id"] == "11111111-1111-1111-1111-111111111111" + assert body["ticker"] == "BTC-PERP" + + def test_market_id_omitted_from_dump_when_none(self) -> None: + req = AmendMarginOrderRequest( + ticker="BTC-PERP", side="bid", price="0.57", count="80" + ) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert "market_id" not in body + + def test_market_id_rejects_non_string(self) -> None: + with pytest.raises(ValidationError): + AmendMarginOrderRequest( + ticker="BTC-PERP", + side="bid", + price="0.57", + count="80", + market_id=["not-a-uuid"], # type: ignore[arg-type] + ) + def test_expiration_time_rejects_negative(self) -> None: with pytest.raises(ValidationError): AmendMarginOrderRequest( @@ -620,16 +708,22 @@ def test_not_retried_on_503(self, perps_client: PerpsClient) -> None: @respx.mock async def test_async(self, async_perps_client: AsyncPerpsClient) -> None: - respx.post(f"{BASE}/margin/orders/ord-1/amend").mock( + route = respx.post(f"{BASE}/margin/orders/ord-1/amend").mock( return_value=httpx.Response(200, json={"order_id": "ord-1"}) ) resp = await async_perps_client.orders.amend( "ord-1", request=AmendMarginOrderRequest( - ticker="BTC-PERP", side="bid", price="0.57", count="80" + ticker="BTC-PERP", + side="bid", + price="0.57", + count="80", + market_id="11111111-1111-1111-1111-111111111111", ), ) assert resp.order_id == "ord-1" + body = json.loads(route.calls[0].request.content) + assert body["market_id"] == "11111111-1111-1111-1111-111111111111" await async_perps_client.close() diff --git a/tests/test_multivariate_models.py b/tests/test_multivariate_models.py index d6c136f..83ab467 100644 --- a/tests/test_multivariate_models.py +++ b/tests/test_multivariate_models.py @@ -2,6 +2,9 @@ from __future__ import annotations +import pytest +from pydantic import ValidationError + from kalshi.models.multivariate import ( CreateMarketResponse, MultivariateEventCollection, @@ -53,6 +56,8 @@ def test_parse_full(self) -> None: assert c.associated_events[0].is_yes_only is True assert c.is_ordered is True assert c.size_min == 2 + assert c.price_level_structure == "binary" + assert c.price_ranges == [] def test_extra_fields_allowed(self) -> None: c = MultivariateEventCollection.model_validate( @@ -75,6 +80,38 @@ def test_extra_fields_allowed(self) -> None: ) assert c.collection_ticker == "T" + def test_parses_price_level_structure_and_price_ranges(self) -> None: + c = MultivariateEventCollection.model_validate( + multivariate_event_collection_dict( + collection_ticker="MVC-1", + price_level_structure="linear_cent", + price_ranges=[ + {"start": "0.01", "end": "0.99", "step": "0.01"}, + ], + ) + ) + assert c.price_level_structure == "linear_cent" + assert c.price_ranges is not None + assert c.price_ranges[0]["step"] == "0.01" + + def test_missing_price_level_structure_raises(self) -> None: + payload = multivariate_event_collection_dict() + del payload["price_level_structure"] + with pytest.raises(ValidationError): + MultivariateEventCollection.model_validate(payload) + + def test_missing_price_ranges_raises(self) -> None: + payload = multivariate_event_collection_dict() + del payload["price_ranges"] + with pytest.raises(ValidationError): + MultivariateEventCollection.model_validate(payload) + + def test_null_price_ranges_coerces_to_empty(self) -> None: + c = MultivariateEventCollection.model_validate( + multivariate_event_collection_dict(price_ranges=None) + ) + assert c.price_ranges == [] + class TestTickerPairModel: def test_parse_and_serialize(self) -> None: