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(