diff --git a/CHANGELOG.md b/CHANGELOG.md index e19a6cc6..78e3d212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,54 @@ All notable changes to kalshi-sdk will be documented in this file. +## 17.0.0 — 2026-09-29 + +Reconciles upstream OpenAPI **3.31.0** content drift plus matching perps and +AsyncAPI updates after nightly contract failures (Closes #522). +**Breaking** for constructors of `Market` that omit the new required +`settlement_bounds_type`. Re-vendored `specs/openapi.yaml` (hash +`dde105932d49e4ea6e197068b82741178ea87edbc991d8fd7676c101162c1c1e`), +`specs/asyncapi.yaml` (hash +`49ea4f5b4f35feba19ba2109e554f6bbcb0a1e123e58feb64e5cfd8182bc72e7`), and +`specs/perps_openapi.yaml` (hash +`bfeb993d2a270e9a319b61d520facacd624e72039c8fe79e45fded10f0578eb7`). + +### Changed (breaking) + +- **`Market.settlement_bounds_type`** (`"default"` / `"floor"`, required) — + which settlement bounds apply. `default` means none. Live + `markets.list` / `markets.get` callers are unaffected; tests/mocks that + construct `Market` must pass the field. + +### Added + +- Optional **`Market.settlement_floor`** (`Decimal | None`), parsed from + `settlement_floor_dollars`. Only filled when `settlement_bounds_type` is + `floor`. +- Optional **`CreateRFQRequest.obscure_creator_id`** and the same kwarg on + `communications.rfqs.create` / deprecated `create_rfq` (sync + async). + Hides the RFQ creator ID from other users until successful execution. + Omit to keep the server default (`false`). +- Optional **`AmendOrderV2Request.expiration_time`** (`int | None`, + minimum 0). int64 Unix seconds. Omit preserves the current expiry; `0` + removes it (good-till-canceled). A nonzero value must be in the future. +- Optional **`AmendMarginOrderRequest.expiration_time`** and + `perps.orders.amend(..., expiration_time=)` (sync + async) — same + semantics as the event-market amend. + +### Spec notes + +- Core OpenAPI `info.version` still **3.31.0** (content-only). Still + unimplemented on the core client: + `POST /portfolio/intra_exchange_instance_transfer`. +- `GET /markets/{ticker}/orderbook` and `GET /markets/orderbooks` dropped + their `security` block upstream. The SDK methods still call + `_require_auth()`; unauthenticated orderbook reads are unchanged. +- AsyncAPI still 15 channels. Description-only updates on communications + creator ids (`"0"` when `obscure_creator_id` is enabled). +- Perps OpenAPI: still 50 operations. Amend-order description now covers + expiration time; queue position is preserved for an expiry-only amend. + ## 16.0.1 — 2026-09-28 Reconciles upstream perps OpenAPI description-only drift on exit-trigger diff --git a/docs/migration.md b/docs/migration.md index 60619076..40d2667e 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,5 +1,40 @@ # Migration +## v16.0 → v17.0.0 + +Reconciles upstream OpenAPI **3.31.0** content drift plus matching perps and +AsyncAPI updates (Closes #522). **Breaking** only for code that constructs +`Market` without `settlement_bounds_type`. + +### Response model field changes + +- **`Market.settlement_bounds_type`** — required `"default"` or `"floor"`. + `floor` markets may also include optional `settlement_floor` (parsed from + `settlement_floor_dollars`). Live `markets.list` / `markets.get` callers + are unaffected; constructors and fixtures must pass the new field. + +```python +# Before (constructors / test fixtures): +# Market(...) # settlement_bounds_type omitted + +# After: +Market(..., settlement_bounds_type="default") +# or "floor", optionally with settlement_floor="0.1000" +``` + +### Added (non-breaking) + +- `CreateRFQRequest.obscure_creator_id` and + `communications.rfqs.create(..., obscure_creator_id=)` — hide the RFQ + creator ID from other users until successful execution. +- `AmendOrderV2Request.expiration_time` — int64 Unix seconds; omit keeps + the current expiry, `0` clears it (good-till-canceled). +- Perps `orders.amend(..., expiration_time=)` / + `AmendMarginOrderRequest.expiration_time` — same semantics. + +See the [changelog](https://github.com/TexasCoding/kalshi-python-sdk/blob/main/CHANGELOG.md) +for the full list. + ## v15.0 → v16.0.0 Reconciles upstream OpenAPI **3.30.0 → 3.31.0** plus matching perps and diff --git a/docs/perps.md b/docs/perps.md index 25a2a603..b6a3ac58 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -70,6 +70,8 @@ async with AsyncPerpsClient.from_env(demo=True) as perps: The margin order side is `bid` / `ask` (not the prediction API's `yes` / `no`). Orders create/cancel/decrease/amend are POSTs/DELETEs and are **never retried**. +`orders.amend(..., expiration_time=)` is int64 Unix seconds: omit it to keep +the current expiry, or pass `0` to clear it (good-till-canceled). !!! 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/communications.md b/docs/resources/communications.md index ff5a69b3..9a96c953 100644 --- a/docs/resources/communications.md +++ b/docs/resources/communications.md @@ -49,6 +49,7 @@ rfq = client.communications.rfqs.create( contracts=500, rest_remainder=True, target_cost_excludes_fees=True, # optional; principal-only target cost + obscure_creator_id=True, # optional; hide creator id until execution ) print(rfq.rfq.rfq_id) diff --git a/docs/resources/orders.md b/docs/resources/orders.md index 4f73dd91..d75c634c 100644 --- a/docs/resources/orders.md +++ b/docs/resources/orders.md @@ -172,6 +172,7 @@ resp = client.orders.amend_v2( price=Decimal("0.66"), count=Decimal("12"), # total/max fillable count exchange_index=0, # body field + expiration_time=0, # optional; 0 clears expiry (GTC) ), ) print(resp.old_order.order_id, resp.order.order_id) diff --git a/kalshi/__init__.py b/kalshi/__init__.py index afb02460..3a03bda1 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -427,4 +427,4 @@ "Withdrawal", ] -__version__ = "16.0.1" +__version__ = "17.0.0" diff --git a/kalshi/models/communications.py b/kalshi/models/communications.py index 505de327..1b667acd 100644 --- a/kalshi/models/communications.py +++ b/kalshi/models/communications.py @@ -170,6 +170,9 @@ class CreateRFQRequest(BaseModel): subtrader_id: str | None = None subaccount: StrictInt | None = Field(default=None, ge=0) target_cost_excludes_fees: bool | None = None + # Hide the creator id from other users until the RFQ executes. Omit keeps + # the server default (false). + obscure_creator_id: bool | None = None model_config = {"extra": "forbid"} diff --git a/kalshi/models/markets.py b/kalshi/models/markets.py index bef662bf..70295072 100644 --- a/kalshi/models/markets.py +++ b/kalshi/models/markets.py @@ -102,6 +102,12 @@ class Market(BaseModel): expected_expiration_time: AwareDatetime | None = None expiration_time: AwareDatetime | None = None settlement_ts: AwareDatetime | None = None + # ``floor`` markets may also send settlement_floor_dollars. + settlement_bounds_type: Literal["default", "floor"] + settlement_floor: DollarDecimal | None = Field( + default=None, + validation_alias=AliasChoices("settlement_floor_dollars", "settlement_floor"), + ) occurrence_datetime: AwareDatetime | None = None # Metadata diff --git a/kalshi/models/orders.py b/kalshi/models/orders.py index 6ec65e81..13cd9505 100644 --- a/kalshi/models/orders.py +++ b/kalshi/models/orders.py @@ -269,6 +269,8 @@ class AmendOrderV2Request(BaseModel): count: FixedPointCount client_order_id: str | None = None 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) exchange_index: StrictInt | None = None model_config = {"extra": "forbid"} diff --git a/kalshi/perps/models/orders.py b/kalshi/perps/models/orders.py index 19a6458c..d80af00b 100644 --- a/kalshi/perps/models/orders.py +++ b/kalshi/perps/models/orders.py @@ -137,6 +137,8 @@ class AmendMarginOrderRequest(BaseModel): count: FixedPointCount = Field(gt=0) client_order_id: str | None = None 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) # ── Response models (extra="allow") ────────────────────────────────────────── diff --git a/kalshi/perps/resources/orders.py b/kalshi/perps/resources/orders.py index b5dbb2d8..fd7dc83a 100644 --- a/kalshi/perps/resources/orders.py +++ b/kalshi/perps/resources/orders.py @@ -162,6 +162,7 @@ def _build_amend_body( count: int | float | str | None, client_order_id: str | None, updated_client_order_id: str | None, + expiration_time: int | None, ) -> dict[str, Any]: _check_request_exclusive( request, @@ -171,6 +172,7 @@ def _build_amend_body( count=count, client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, + expiration_time=expiration_time, ) if request is None: if ticker is None or side is None or price is None or count is None: @@ -185,6 +187,7 @@ def _build_amend_body( count=to_decimal(count), client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, + expiration_time=expiration_time, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -472,6 +475,7 @@ def amend( count: int | float | str, client_order_id: str | None = ..., updated_client_order_id: str | None = ..., + expiration_time: int | None = ..., subaccount: int | None = ..., extra_headers: dict[str, str] | None = None, ) -> AmendMarginOrderResponse: ... @@ -486,6 +490,7 @@ def amend( count: int | float | str | None = None, client_order_id: str | None = None, updated_client_order_id: str | None = None, + expiration_time: int | None = None, subaccount: int | None = None, extra_headers: dict[str, str] | None = None, ) -> AmendMarginOrderResponse: @@ -493,6 +498,10 @@ def amend( ``subaccount`` is a query param. Increasing size or changing price forfeits queue position (server-side). Not retried. + + ``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. """ self._require_auth() body = _build_amend_body( @@ -503,6 +512,7 @@ def amend( count=count, client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, + expiration_time=expiration_time, ) params = _params(subaccount=subaccount) data = self._post( @@ -813,6 +823,7 @@ async def amend( count: int | float | str, client_order_id: str | None = ..., updated_client_order_id: str | None = ..., + expiration_time: int | None = ..., subaccount: int | None = ..., extra_headers: dict[str, str] | None = None, ) -> AmendMarginOrderResponse: ... @@ -827,6 +838,7 @@ async def amend( count: int | float | str | None = None, client_order_id: str | None = None, updated_client_order_id: str | None = None, + expiration_time: int | None = None, subaccount: int | None = None, extra_headers: dict[str, str] | None = None, ) -> AmendMarginOrderResponse: @@ -840,6 +852,7 @@ async def amend( count=count, client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, + expiration_time=expiration_time, ) params = _params(subaccount=subaccount) data = await self._post( diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index 9b85f913..f1a7f09d 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -146,6 +146,7 @@ def _build_create_rfq_body( subtrader_id: str | None, subaccount: int | None, target_cost_excludes_fees: bool | None, + obscure_creator_id: bool | None, ) -> dict[str, Any]: _check_request_exclusive( request, @@ -157,6 +158,7 @@ def _build_create_rfq_body( subtrader_id=subtrader_id, subaccount=subaccount, target_cost_excludes_fees=target_cost_excludes_fees, + obscure_creator_id=obscure_creator_id, ) if request is None: if market_ticker is None or rest_remainder is None: @@ -172,6 +174,7 @@ def _build_create_rfq_body( subtrader_id=subtrader_id, subaccount=subaccount, target_cost_excludes_fees=target_cost_excludes_fees, + obscure_creator_id=obscure_creator_id, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -426,6 +429,7 @@ def create( subtrader_id: str | None = ..., subaccount: int | None = ..., target_cost_excludes_fees: bool | None = ..., + obscure_creator_id: bool | None = ..., extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: ... def create( @@ -440,6 +444,7 @@ def create( subtrader_id: str | None = None, subaccount: int | None = None, target_cost_excludes_fees: bool | None = None, + obscure_creator_id: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: self._require_auth() @@ -453,6 +458,7 @@ def create( subtrader_id=subtrader_id, subaccount=subaccount, target_cost_excludes_fees=target_cost_excludes_fees, + obscure_creator_id=obscure_creator_id, ) data = self._post("/communications/rfqs", json=body, extra_headers=extra_headers) return CreateRFQResponse.model_validate(data) @@ -1011,6 +1017,7 @@ def create_rfq( subtrader_id: str | None = None, subaccount: int | None = None, target_cost_excludes_fees: bool | None = None, + obscure_creator_id: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.create` instead.""" @@ -1024,6 +1031,7 @@ def create_rfq( subtrader_id=subtrader_id, subaccount=subaccount, target_cost_excludes_fees=target_cost_excludes_fees, + obscure_creator_id=obscure_creator_id, extra_headers=extra_headers, ) @@ -1250,6 +1258,7 @@ async def create( subtrader_id: str | None = ..., subaccount: int | None = ..., target_cost_excludes_fees: bool | None = ..., + obscure_creator_id: bool | None = ..., extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: ... async def create( @@ -1264,6 +1273,7 @@ async def create( subtrader_id: str | None = None, subaccount: int | None = None, target_cost_excludes_fees: bool | None = None, + obscure_creator_id: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: self._require_auth() @@ -1277,6 +1287,7 @@ async def create( subtrader_id=subtrader_id, subaccount=subaccount, target_cost_excludes_fees=target_cost_excludes_fees, + obscure_creator_id=obscure_creator_id, ) data = await self._post("/communications/rfqs", json=body, extra_headers=extra_headers) return CreateRFQResponse.model_validate(data) @@ -1827,6 +1838,7 @@ async def create_rfq( subtrader_id: str | None = None, subaccount: int | None = None, target_cost_excludes_fees: bool | None = None, + obscure_creator_id: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.create` instead.""" @@ -1840,6 +1852,7 @@ async def create_rfq( subtrader_id=subtrader_id, subaccount=subaccount, target_cost_excludes_fees=target_cost_excludes_fees, + obscure_creator_id=obscure_creator_id, extra_headers=extra_headers, ) diff --git a/pyproject.toml b/pyproject.toml index b3897536..90b88a94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "16.0.1" +version = "17.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/scripts/bench_page_to_dataframe.py b/scripts/bench_page_to_dataframe.py index 9b347ad8..87df2333 100644 --- a/scripts/bench_page_to_dataframe.py +++ b/scripts/bench_page_to_dataframe.py @@ -53,6 +53,7 @@ def _market_dict(ticker: str) -> dict[str, Any]: "close_time": "2026-12-31T23:59:59Z", "latest_expiration_time": "2026-12-31T23:59:59Z", "settlement_timer_seconds": 0, + "settlement_bounds_type": "default", "result": "", "can_close_early": False, "fractional_trading_enabled": False, diff --git a/specs/asyncapi.yaml b/specs/asyncapi.yaml index 2d6590f1..802e91e8 100644 --- a/specs/asyncapi.yaml +++ b/specs/asyncapi.yaml @@ -3753,7 +3753,7 @@ components: description: Unique identifier for the RFQ creator_id: type: string - description: Public communications ID of the RFQ creator (anonymized). + description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled. market_ticker: type: string description: Market ticker for the RFQ @@ -3815,7 +3815,7 @@ components: description: Unique identifier for the RFQ creator_id: type: string - description: Public communications ID of the RFQ creator (anonymized) + description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled. market_ticker: type: string description: Market ticker for the RFQ @@ -3859,7 +3859,7 @@ components: description: Public communications ID of the quote creator (anonymized) rfq_creator_id: type: string - description: Public communications ID of the RFQ creator (anonymized) + description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled. market_ticker: type: string description: Market ticker for the quote @@ -3917,7 +3917,7 @@ components: description: Public communications ID of the quote creator (anonymized) rfq_creator_id: type: string - description: Public communications ID of the RFQ creator (anonymized) + description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled. market_ticker: type: string description: Market ticker for the quote diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 979287a0..57730170 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -222,10 +222,6 @@ paths: description: ' Endpoint for getting the current order book for a specific market. The order book shows all active bid orders for both yes and no sides of a binary market. It returns yes bids and no bids only (no asks are returned). This is because in binary markets, a bid for yes at price X is equivalent to an ask for no at price (100-X). For example, a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract sizes. Each side shows price levels with their corresponding quantities and order counts, organized from best to worst prices.' tags: - market - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] parameters: - $ref: '#/components/parameters/TickerPath' - name: depth @@ -260,10 +256,6 @@ paths: description: 'Endpoint for getting the current order books for multiple markets in a single request. The order book shows all active bid orders for both yes and no sides of a binary market. It returns yes bids and no bids only (no asks are returned). This is because in binary markets, a bid for yes at price X is equivalent to an ask for no at price (100-X). For example, a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract sizes. Each side shows price levels with their corresponding quantities and order counts, organized from best to worst prices. Returns one orderbook per requested market ticker.' tags: - market - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] parameters: - name: tickers in: query @@ -1220,11 +1212,11 @@ paths: post: operationId: AmendOrderV2 summary: Amend Order (V2) - description: 'Endpoint for amending the price and/or max fillable count of an existing event-market order using the V2 request/response shape. The request `count` is the updated total/max fillable count, equal to already filled count plus desired resting remaining count. This behavior matches the v1 amend endpoints; only the request/response shape differs.' + description: 'Endpoint for amending the price, max fillable count, and/or expiration time of an existing event-market order. The request `count` is the updated total/max fillable count, equal to already filled count plus desired resting remaining count. This behavior matches the v1 amend endpoints; only the request/response shape differs.' x-mint: content: | - Amending a resting order preserves queue position only when the amendment decreases size. All other amendments — like increasing size or changing price forfeit queue position and place the order at the back of the queue. + Amending only expiry or decreasing size preserves queue position. Increasing size or changing price forfeits queue position and places the order at the back of the queue. tags: - orders @@ -7812,7 +7804,7 @@ components: description: UUID of the RFQ. Preserve the exact returned string. creator_id: type: string - description: Public communications ID of the RFQ creator. + description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled. market_ticker: type: string description: The ticker of the market this RFQ is for @@ -7930,6 +7922,11 @@ components: Kalshi fees, and quote sizes are reduced to make room for the fees. Only valid together with a target cost. x-go-type-skip-optional-pointer: true + obscure_creator_id: + type: boolean + description: Hide the RFQ creator ID from other users until successful execution. The creator always sees their own ID. + default: false + x-go-type-skip-optional-pointer: true rest_remainder: type: boolean description: Whether to rest the remainder of the RFQ after execution @@ -7981,7 +7978,7 @@ components: description: Public communications ID of the quote creator rfq_creator_id: type: string - description: Public communications ID of the RFQ creator + description: Public communications ID of the RFQ creator (anonymized). Set to "0" for other users when obscure_creator_id is enabled. x-go-type-skip-optional-pointer: true market_ticker: type: string @@ -8693,6 +8690,15 @@ components: type: string description: The new client-specified order ID after amendment x-go-type-skip-optional-pointer: true + expiration_time: + type: integer + format: int64 + minimum: 0 + description: >- + New Unix expiration timestamp in seconds. Omit to preserve the current + expiry, or use 0 to remove it (good-till-canceled). A nonzero expiry + must be in the future. Send the current price and total quantity for + an expiry-only amendment; this preserves queue position. exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' @@ -9693,6 +9699,7 @@ components: - rules_secondary - price_level_structure - price_ranges + - settlement_bounds_type properties: ticker: type: string @@ -9809,6 +9816,15 @@ components: nullable: true x-omitempty: true description: Timestamp when the market was settled. Only filled for settled markets + settlement_bounds_type: + type: string + enum: [default, floor] + description: Which settlement bounds apply to this market. default means the market has no settlement bounds + settlement_floor_dollars: + $ref: '#/components/schemas/FixedPointDollars' + nullable: true + x-omitempty: true + description: The lowest value the YES/LONG side of the contract can settle at in dollars. Only filled when settlement_bounds_type is floor expiration_value: type: string description: The value that was considered for the settlement diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index f57ba28c..85a597ab 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -516,11 +516,11 @@ paths: post: operationId: AmendMarginOrder summary: Amend Order - description: Endpoint for amending the price and/or max number of fillable contracts in an existing margin order. + description: Endpoint for amending the price, max number of fillable contracts, and/or expiration time of an existing margin order. x-mint: content: | - Amending a resting order preserves queue position only when the amendment decreases size. All other amendments — like increasing size or changing price forfeit queue position and place the order at the back of the queue. + Amending only expiry or decreasing size preserves queue position. Increasing size or changing price forfeits queue position and places the order at the back of the queue. tags: - orders @@ -2772,6 +2772,16 @@ components: type: string description: The new client-specified order ID after amendment x-go-type-skip-optional-pointer: true + expiration_time: + type: integer + format: int64 + minimum: 0 + description: >- + New Unix expiration timestamp in seconds. Omit to preserve the current + expiry, or use 0 to remove it (good-till-canceled). A nonzero expiry + must be in the future. Send the current price and total quantity for + an expiry-only amendment; this preserves queue position. + AmendMarginOrderResponse: type: object diff --git a/tests/_model_fixtures.py b/tests/_model_fixtures.py index 6753b6d2..b8ad02ee 100644 --- a/tests/_model_fixtures.py +++ b/tests/_model_fixtures.py @@ -57,6 +57,7 @@ def market_dict(**overrides: Any) -> dict[str, Any]: "close_time": "2026-12-31T23:59:59Z", "latest_expiration_time": "2026-12-31T23:59:59Z", "settlement_timer_seconds": 0, + "settlement_bounds_type": "default", "result": "", "can_close_early": False, "fractional_trading_enabled": False, diff --git a/tests/perps/test_orders.py b/tests/perps/test_orders.py index 780c461b..698c36e8 100644 --- a/tests/perps/test_orders.py +++ b/tests/perps/test_orders.py @@ -534,6 +534,32 @@ def test_happy_with_fills(self, perps_client: PerpsClient) -> None: assert resp.fill_count == Decimal("50.00") assert resp.average_fill_price == Decimal("0.5700") + @respx.mock + def test_expiration_time_zero_is_sent(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", + expiration_time=0, + ) + body = json.loads(route.calls[0].request.content) + assert body["expiration_time"] == 0 + + def test_expiration_time_rejects_negative(self) -> None: + with pytest.raises(ValidationError): + AmendMarginOrderRequest( + ticker="BTC-PERP", + side="bid", + price="0.57", + count="80", + expiration_time=-1, + ) + @respx.mock def test_subaccount_query_param(self, perps_client: PerpsClient) -> None: route = respx.post(f"{BASE}/margin/orders/ord-1/amend").mock( diff --git a/tests/test_communications.py b/tests/test_communications.py index 716d3053..b02d2484 100644 --- a/tests/test_communications.py +++ b/tests/test_communications.py @@ -191,6 +191,24 @@ def test_create_rfq_request_omits_optional_fields(self) -> None: body = req.model_dump(exclude_none=True, by_alias=True, mode="json") assert body == {"market_ticker": "MKT-1", "rest_remainder": False} + def test_create_rfq_request_serializes_obscure_creator_id(self) -> None: + req = CreateRFQRequest( + market_ticker="MKT-1", + rest_remainder=True, + obscure_creator_id=True, + ) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert body["obscure_creator_id"] is True + + def test_create_rfq_request_omits_obscure_creator_id_when_none(self) -> None: + req = CreateRFQRequest( + market_ticker="MKT-1", + rest_remainder=True, + obscure_creator_id=None, + ) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert "obscure_creator_id" not in body + def test_create_rfq_request_serializes_target_cost_excludes_fees(self) -> None: req = CreateRFQRequest( market_ticker="MKT-1", @@ -372,6 +390,26 @@ def test_sends_correct_body(self, comms: CommunicationsResource) -> None: "subaccount": 2, } + @respx.mock + def test_sends_obscure_creator_id(self, comms: CommunicationsResource) -> None: + route = respx.post( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(201, json={"id": "rfq-new"})) + comms.rfqs.create( + market_ticker="MKT-1", + rest_remainder=True, + obscure_creator_id=True, + ) + with pytest.warns(DeprecationWarning): + comms.create_rfq( + market_ticker="MKT-1", + rest_remainder=True, + obscure_creator_id=True, + ) + for call in route.calls: + body = json.loads(call.request.content) + assert body["obscure_creator_id"] is True + @respx.mock def test_sends_target_cost_excludes_fees(self, comms: CommunicationsResource) -> None: route = respx.post( diff --git a/tests/test_models.py b/tests/test_models.py index b587ddf6..6e0e0b4d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -92,6 +92,25 @@ def test_order_decimal_fields(self) -> None: assert o.no_price == Decimal("0.35") +class TestMarketSettlementBounds: + """Spec-required settlement_bounds_type plus optional settlement floor.""" + + def test_default_bounds_and_absent_floor(self) -> None: + m = Market.model_validate(market_dict(settlement_bounds_type="default")) + assert m.settlement_bounds_type == "default" + assert m.settlement_floor is None + + def test_floor_parses_settlement_floor_dollars(self) -> None: + m = Market.model_validate( + market_dict( + settlement_bounds_type="floor", + settlement_floor_dollars="0.1000", + ) + ) + assert m.settlement_bounds_type == "floor" + assert m.settlement_floor == Decimal("0.1000") + + class TestMarketOccurrenceDatetime: """Round-trip the `occurrence_datetime` field added in spec v3.13.x. diff --git a/tests/test_orders.py b/tests/test_orders.py index a0065de5..7c6b3db7 100644 --- a/tests/test_orders.py +++ b/tests/test_orders.py @@ -9,6 +9,7 @@ import httpx import pytest import respx +from pydantic import ValidationError from kalshi._base_client import SyncTransport from kalshi.auth import KalshiAuth @@ -597,6 +598,27 @@ def test_returns_response(self, orders: OrdersResource) -> None: ) assert result.order_id == "ord-1" + def test_expiration_time_zero_clears_expiry(self) -> None: + req = AmendOrderV2Request( + ticker="MKT-A", + side="bid", + price=Decimal("0.55"), + count=Decimal("10"), + expiration_time=0, + ) + body = req.model_dump(exclude_none=True, mode="json") + assert body["expiration_time"] == 0 + + def test_expiration_time_rejects_negative(self) -> None: + with pytest.raises(ValidationError): + AmendOrderV2Request( + ticker="MKT-A", + side="bid", + price=Decimal("0.55"), + count=Decimal("10"), + expiration_time=-1, + ) + def test_side_must_be_bid_or_ask(self) -> None: with pytest.raises(ValueError): AmendOrderV2Request(