diff --git a/CHANGELOG.md b/CHANGELOG.md index 42ea44e..47fbfce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to kalshi-sdk will be documented in this file. +## 22.4.0 — 2026-10-26 + +Reconciles nightly spec drift (#556, #557). Additive on the RFQ-batch +child request and on perps `MarginMarket`. No breaking changes. + +### Added + +- Optional **`CreateRFQBatchChild.contracts`** / + **`CreateRFQBatchChild.target_cost`** — per-child size in place of + the batch-level size. Python names drop the `_fp` / `_dollars` + suffixes; serialize as `contracts_fp` / `target_cost_dollars` and + accept both wire and Python names. Batch-level `contracts` / + `target_cost` still apply to every child that sets no size of its + own. (Closes #557). + +- Optional **`MarginMarket.halt_sources`** (`list[str] | None`). The + spec marks it required; the SDK keeps it optional so this stays a + minor release. Why the + market is halted (`"initial_setup"`, `"corporate_action"`, …). Empty + means the market is not halted or the reason has not been observed — + do not read empty as proof the market is live. (Closes #557). + +### Changed + +- Re-vendored `specs/` (OpenAPI 3.34.0 still). Core AsyncAPI dropped + the `batch_communications` channel (16 → 15); RFQ-batch notifications + now ride `communications`. The SDK never shipped a typed + `subscribe_batch_communications` helper — `subscribe_communications()` + documents the move. Perps AsyncAPI and Klear SCM are unchanged. + (Closes #556). + ## 22.3.0 — 2026-10-22 Reconciles nightly spec drift (#553). Maps the ten core RFQ batch diff --git a/CLAUDE.md b/CLAUDE.md index 30d1753..c2b3ab9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -123,7 +123,7 @@ tests/ ## API Reference - OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.34.0, 128 operations; 127 mapped in the core SDK — `POST /portfolio/intra_exchange_instance_transfer` is implemented on `PerpsClient.transfers.transfer_instance` and left unimplemented on the core client) -- AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (16 WebSocket channels; 12 typed `subscribe_*` + escape-hatch) +- AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (15 WebSocket channels; 12 typed `subscribe_*` + escape-hatch) - Base URL: https://api.elections.kalshi.com/trade-api/v2 - Demo URL: https://demo-api.kalshi.co/trade-api/v2 - Auth: RSA-PSS / SHA256 / MGF1(SHA256) / salt_length=DIGEST_LENGTH / base64 diff --git a/docs/perps.md b/docs/perps.md index 1d00021..b5c58f1 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -189,7 +189,11 @@ perps.portfolio.cancel_cross_exit_trigger("BTC-PERP", trig.id) (`"dampen"` / `"exclude"`): how funding averages a second with no premium (no orderbook sample, no index price, or a trading halt). `dampen` counts that second as a premium of 0; `exclude` leaves it - out of the average. + out of the average. `MarginMarket.halt_sources` is optional + (`list[str] | None`; spec-required, `None` if absent): why the market is halted (`"initial_setup"`, + `"corporate_action"`, …). Empty means the market is not halted or + the reason has not been observed — do not read empty as proof the + market is live. - **Mark prices** (since the v3.21.0 spec sync) — `MarginMarket` carries three optional reference-price objects: `settlement_mark_price`, `liquidation_mark_price`, and `reference_price`, each a nested `TickerPrice` diff --git a/docs/reference.md b/docs/reference.md index edd2c58..cd8aa3e 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -62,6 +62,8 @@ every exception class. ::: kalshi.models.communications.CreateRFQBatchRequest +::: kalshi.models.communications.CreateRFQBatchChild + ::: kalshi.models.communications.CreateRFQBatchQuoteRequest ::: kalshi.models.communications.AcceptRFQBatchRequest diff --git a/docs/request-models.md b/docs/request-models.md index d6344e7..f21ca58 100644 --- a/docs/request-models.md +++ b/docs/request-models.md @@ -63,6 +63,7 @@ exposed by each resource method stays in lockstep with the OpenAPI spec. | `client.communications.accept_quote` | `AcceptQuoteRequest` | | `client.communications.block_trade_proposals.create` | `ProposeBlockTradeRequest` | | `client.communications.block_trade_proposals.accept` | `AcceptBlockTradeProposalRequest` | +| `client.communications.rfq_batches.create` | `CreateRFQBatchRequest` (wraps `list[CreateRFQBatchChild]`) | | `client.multivariate_collections.create_market` | `CreateMarketInMultivariateEventCollectionRequest` | | `client.order_groups.create` | `CreateOrderGroupRequest` | | `client.order_groups.update_limit` | `UpdateOrderGroupLimitRequest` | @@ -79,6 +80,8 @@ an awkward wire name: | Model field | Wire name | |---|---| | `CreateRFQRequest.target_cost` | `target_cost_dollars` | +| `CreateRFQBatchRequest.contracts` / `CreateRFQBatchChild.contracts` | `contracts_fp` | +| `CreateRFQBatchRequest.target_cost` / `CreateRFQBatchChild.target_cost` | `target_cost_dollars` | | `StructuredTarget.target_type` (response) | `type` (avoids shadowing the builtin) | You never type these long names — they're an internal implementation detail. diff --git a/docs/resources/communications.md b/docs/resources/communications.md index 5a4d1fd..4f50403 100644 --- a/docs/resources/communications.md +++ b/docs/resources/communications.md @@ -229,14 +229,14 @@ batch; each child is accepted from whichever maker quotes it best on your side. Accepting fills every child or none. ```python -# Create a batch of two combos, sized the same way. +# Create a batch of two combos. Size the batch, or set contracts / +# target_cost on each child in place of the batch-level size. resp = client.communications.rfq_batches.create( children=[ - {"market_ticker": "COMBO-A"}, - {"market_ticker": "COMBO-B"}, + {"market_ticker": "COMBO-A", "contracts": "10.00"}, + {"market_ticker": "COMBO-B", "contracts": "5.00"}, ], - contracts="10.00", - rest_remainder=True, + rest_remainder=False, ) batch_id = resp.rfq_batch.id @@ -272,9 +272,11 @@ you on that one batch. Prices on create-quote entries use `yes_bid` / `no_bid` (no `_dollars` suffix), matching `quotes.create()`. Accept `min_bids` use Python name -`bid`, serialized as `bid_dollars`. Batch create sizes every child via +`bid`, serialized as `bid_dollars`. Batch create sizes children via `contracts` (`contracts_fp` on the wire) or `target_cost` -(`target_cost_dollars`). +(`target_cost_dollars`) on the batch (applied to every child that sets +no size of its own) or on each `CreateRFQBatchChild`. `rest_remainder` +must be false: a batch executes as one fill-or-kill. ## Migrating from v2.x diff --git a/docs/websockets.md b/docs/websockets.md index 4a2ffbd..31e945c 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -50,6 +50,10 @@ SDK's perspective on it. The `type` column matters when filtering raw logs — note the singular forms for `user_order` and `market_position`. The standalone `multivariate` / `multivariate_lookup` channel was removed from AsyncAPI (SDK v10.0.0). +The standalone `batch_communications` channel was removed from AsyncAPI +(SDK v22.4.0, 16 → 15 spec channels); RFQ-batch notifications now ride +`communications`. The SDK never shipped a typed +`subscribe_batch_communications` helper. !!! warning "Migration (v3.1.0): `event_fee_update` rides `market_lifecycle_v2`" Since the v3.20.0 spec sync (SDK v3.1.0) the `market_lifecycle_v2` channel diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 5877c09..4e58169 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -477,4 +477,4 @@ "Withdrawal", ] -__version__ = "22.3.0" +__version__ = "22.4.0" diff --git a/kalshi/_contract_map.py b/kalshi/_contract_map.py index cef3898..13914c7 100644 --- a/kalshi/_contract_map.py +++ b/kalshi/_contract_map.py @@ -266,6 +266,7 @@ class ContractEntry: ContractEntry( sdk_model="kalshi.models.communications.CreateRFQBatchChild", spec_schema="CreateRFQBatchChild", + notes="Uses serialization_alias + AliasChoices for contracts_fp / target_cost_dollars", ), ContractEntry( sdk_model="kalshi.models.communications.RFQBatchQuoteEntry", diff --git a/kalshi/models/communications.py b/kalshi/models/communications.py index f65ba55..ce7174b 100644 --- a/kalshi/models/communications.py +++ b/kalshi/models/communications.py @@ -385,10 +385,24 @@ class GetRFQBatchesResponse(BaseModel): class CreateRFQBatchChild(BaseModel): """One combo to request quotes on. Set exactly one of ``market_ticker`` and ``selected_markets``; the spec does not encode the exclusivity. + + Optional ``contracts`` / ``target_cost`` size this child in place of + the batch-level size. Provide at most one; the spec does not encode + the exclusivity, so the SDK does not enforce it. """ market_ticker: str | None = None selected_markets: list[TickerPair] | None = None + contracts: FixedPointCount | None = Field( + default=None, + serialization_alias="contracts_fp", + validation_alias=AliasChoices("contracts_fp", "contracts"), + ) + target_cost: DollarDecimal | None = Field( + default=None, + serialization_alias="target_cost_dollars", + validation_alias=AliasChoices("target_cost_dollars", "target_cost"), + ) model_config = {"extra": "forbid"} @@ -396,11 +410,12 @@ class CreateRFQBatchChild(BaseModel): class CreateRFQBatchRequest(BaseModel): """Body for POST /communications/rfq_batches. - Spec sizes every child the same way via ``contracts_fp`` or - ``target_cost_dollars``. SDK Python names drop those suffixes and - serialize with ``serialization_alias``. Provide exactly one of - ``contracts`` and ``target_cost``; the spec does not encode the - exclusivity, so the SDK does not enforce it. + Spec sizes children via ``contracts_fp`` or ``target_cost_dollars`` + on the batch (applied to every child that sets no size of its own) + or on each child. SDK Python names drop those suffixes and serialize + with ``serialization_alias``. Provide at most one of ``contracts`` + and ``target_cost``; the spec does not encode the exclusivity, so + the SDK does not enforce it. """ children: list[CreateRFQBatchChild] diff --git a/kalshi/perps/models/markets.py b/kalshi/perps/models/markets.py index e574537..fd807fb 100644 --- a/kalshi/perps/models/markets.py +++ b/kalshi/perps/models/markets.py @@ -103,6 +103,13 @@ class MarginMarket(BaseModel): # index price, or a trading halt), configured at market creation. # ``dampen`` counts the second as premium 0; ``exclude`` omits it. funding_invalid_premium_handling: Literal["dampen", "exclude"] + # Spec marks it required; the SDK keeps it optional so older payloads + # and existing constructors still validate (minor release). Why the + # market is halted (``"initial_setup"``, + # ``"corporate_action"``, …). Empty means the market is not halted or + # the reason has not been observed — do not read empty as proof the + # market is live. + halt_sources: list[str] | None = None leverage_estimate: MultiplierDecimal | None = None # Leverage (1 / margin_rate) keyed by notional position size in dollars diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index bbbe1b2..4ee40cf 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -1257,6 +1257,12 @@ def create( subaccount: int | None = None, extra_headers: dict[str, str] | None = None, ) -> CreateRFQBatchResponse: + """Create an RFQ batch. + + Size the batch via ``contracts`` / ``target_cost``, or set + ``contracts`` / ``target_cost`` on each :class:`CreateRFQBatchChild` + in place of the batch-level size. + """ self._require_auth() body = _build_create_rfq_batch_body( request, @@ -2360,6 +2366,12 @@ async def create( subaccount: int | None = None, extra_headers: dict[str, str] | None = None, ) -> CreateRFQBatchResponse: + """Create an RFQ batch. + + Size the batch via ``contracts`` / ``target_cost``, or set + ``contracts`` / ``target_cost`` on each :class:`CreateRFQBatchChild` + in place of the batch-level size. + """ self._require_auth() body = _build_create_rfq_batch_body( request, diff --git a/kalshi/ws/client.py b/kalshi/ws/client.py index faed3f3..972281c 100644 --- a/kalshi/ws/client.py +++ b/kalshi/ws/client.py @@ -872,6 +872,14 @@ async def subscribe_communications( shard_key: int | None = None, maxsize: int = 1000, ) -> AsyncIterator[CommunicationsMessage]: + """Subscribe to RFQ and quote notifications. + + RFQ-batch notifications (``rfq_batch_created``, ``rfq_batch_deleted``, + ``batch_quote_*``, ``rfq_batch_best_quotes``, ``rfq_batch_executed``) + now arrive on this channel. The standalone ``batch_communications`` + channel was removed from the AsyncAPI spec (SDK v22.4.0); the SDK + never shipped a typed subscribe helper for it. + """ params: dict[str, Any] = {} if shard_factor is not None: params["shard_factor"] = shard_factor diff --git a/kalshi/ws/models/communications.py b/kalshi/ws/models/communications.py index fdcebd1..6e2230c 100644 --- a/kalshi/ws/models/communications.py +++ b/kalshi/ws/models/communications.py @@ -163,6 +163,10 @@ class CommunicationsMessage(BaseModel): Users who want typed parsing can use the individual payload models (RfqCreatedPayload, QuoteCreatedPayload, etc.) to validate msg contents. + + RFQ-batch notifications now arrive on this channel. The standalone + ``batch_communications`` channel was removed from the AsyncAPI spec + (SDK v22.4.0). """ type: Literal["communications"] = "communications" diff --git a/pyproject.toml b/pyproject.toml index 3ede4f6..36cb726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "22.3.0" +version = "22.4.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 bce31dc..f82ca5f 100644 --- a/specs/asyncapi.yaml +++ b/specs/asyncapi.yaml @@ -298,8 +298,12 @@ channels: - `shard_factor` (1-100) and `shard_key` (`0 <= key < shard_factor`) - RFQ events (RFQCreated, RFQDeleted) are sent for all users by default. Set `user_filter: "self"` to receive only RFQs you created. Omitted or empty `user_filter` keeps the default. - Quote events (QuoteCreated, QuoteAccepted, QuoteExecuted) are only sent if you created the quote OR you created the RFQ + - RFQ batch events (RFQBatchCreated, RFQBatchDeleted) follow the RFQ event rules: sent for all users by default, or with `user_filter: "self"` only for RFQ batches you created. A batch's child RFQs are announced only inside RFQBatchCreated; each child trade still sends QuoteExecuted, with `rfq_batch_id` set. With sharding, every batch event is partitioned by `rfq_batch_id` instead of the market ticker, so a child's QuoteExecuted can arrive on another shard than its batch's events. + - Each child of an RFQ batch fills at its best quote across makers, and the children execute together as one fill-or-kill: nothing trades unless every child can fill in full. In the rare case that filling one child cancels a maker order another child needed, the batch closes partially executed. Every maker's batch quote prices every child, and each child goes to the maker bidding best on the accepted side, so one batch can have several winning makers. A batch quote counts only for 30 seconds after it was last submitted; resending an unchanged bid within that time keeps its time priority. + - RFQBatchBestQuotes and RFQBatchExecuted are only sent to the RFQ batch creator. RFQBatchBestQuotes carries the best bids across makers without identifying them; the batch creator never receives a maker's individual batch quote. + - Batch quote events (BatchQuoteCreated, BatchQuoteAccepted, BatchQuoteExecuted) are only sent to the maker that created the batch quote. Each winning maker confirms the children it won, and nothing executes until every winning maker has confirmed. - **Use case:** Tracking RFQs you create and quotes on your RFQs, or quotes you create on others' RFQs. Use QuoteExecuted to correlate fill messages with quotes via client_order_id. + **Use case:** Tracking RFQs and RFQ batches you create and the quotes on them, or quotes and batch quotes you create on others'. Use QuoteExecuted to correlate fill messages with quotes via client_order_id. messages: rfqCreated: $ref: '#/components/messages/rfqCreated' @@ -311,27 +315,6 @@ channels: $ref: '#/components/messages/quoteAccepted' quoteExecuted: $ref: '#/components/messages/quoteExecuted' - - batch_communications: - address: batch_communications - title: Batch Communications - description: | - Real-time RFQ batch and batch quote notifications. Requires authentication. - - **Requirements:** - - Authentication required - - Market specification ignored - - Optional sharding for fanout control: - - `shard_factor` (1-100) and `shard_key` (`0 <= key < shard_factor`) - - Every event is partitioned by `rfq_batch_id` - - RFQ batch events (RFQBatchCreated, RFQBatchDeleted) are sent for all users by default. Set `user_filter: "self"` to receive only RFQ batches you created. Omitted or empty `user_filter` keeps the default. - - A batch's child RFQs are announced only inside RFQBatchCreated. Each child trade still sends QuoteExecuted, with `rfq_batch_id` set, on the `communications` channel. The two channels are separate subscriptions, so a child's QuoteExecuted is not ordered with this channel's messages. - - An RFQ batch is filled child by child at the best quote across makers. Every maker's batch quote prices every child, and each child goes to the maker bidding best on the accepted side, so one batch can have several winning makers. A batch quote counts only for 30 seconds after it was last submitted; resending an unchanged bid within that time keeps its time priority. - - RFQBatchBestQuotes and RFQBatchExecuted are only sent to the RFQ batch creator. RFQBatchBestQuotes carries the best bids across makers without identifying them; the batch creator never receives a maker's individual batch quote. - - Batch quote events (BatchQuoteCreated, BatchQuoteAccepted, BatchQuoteExecuted) are only sent to the maker that created the batch quote. Each winning maker confirms the children it won, and nothing executes until every winning maker has confirmed. - - **Use case:** Tracking RFQ batches you create and the best quotes on them, or batch quotes you create on others' RFQ batches. - messages: rfqBatchCreated: $ref: '#/components/messages/rfqBatchCreated' rfqBatchDeleted: @@ -1067,9 +1050,9 @@ operations: title: RFQ Batch Created summary: Receive RFQ batch created notifications channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/rfqBatchCreated' + - $ref: '#/channels/communications/messages/rfqBatchCreated' tags: - name: private @@ -1078,9 +1061,9 @@ operations: title: RFQ Batch Deleted summary: Receive RFQ batch deleted notifications channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/rfqBatchDeleted' + - $ref: '#/channels/communications/messages/rfqBatchDeleted' tags: - name: private @@ -1089,9 +1072,9 @@ operations: title: Batch Quote Created summary: Receive batch quote created notifications channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/batchQuoteCreated' + - $ref: '#/channels/communications/messages/batchQuoteCreated' tags: - name: private @@ -1100,9 +1083,9 @@ operations: title: RFQ Batch Best Quotes summary: Receive the best quotes across makers on your RFQ batch channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/rfqBatchBestQuotes' + - $ref: '#/channels/communications/messages/rfqBatchBestQuotes' tags: - name: private @@ -1111,9 +1094,9 @@ operations: title: Batch Quote Accepted summary: Receive batch quote accepted notifications channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/batchQuoteAccepted' + - $ref: '#/channels/communications/messages/batchQuoteAccepted' tags: - name: private @@ -1122,9 +1105,9 @@ operations: title: RFQ Batch Executed summary: Receive RFQ batch executed notifications channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/rfqBatchExecuted' + - $ref: '#/channels/communications/messages/rfqBatchExecuted' tags: - name: private @@ -1133,9 +1116,9 @@ operations: title: Batch Quote Executed summary: Receive batch quote executed notifications channel: - $ref: '#/channels/batch_communications' + $ref: '#/channels/communications' messages: - - $ref: '#/channels/batch_communications/messages/batchQuoteExecuted' + - $ref: '#/channels/communications/messages/batchQuoteExecuted' tags: - name: private @@ -2651,10 +2634,10 @@ components: rfqBatchExecuted: name: rfq_batch_executed title: RFQ Batch Executed - summary: Notification when every child of your accepted RFQ batch has executed or failed + summary: Notification when your accepted RFQ batch has executed or failed description: | - Sent only to the RFQ batch creator, once per batch, when every child has executed or failed. Each child trade also sends `quote_executed` on the `communications` channel, a separate subscription, so it is not ordered with this message. - Children that failed to execute are listed in `failed_rfq_ids`. Each winning maker receives `batch_quote_executed` for its own children instead. + Sent only to the RFQ batch creator, once per batch, after the per-child `quote_executed` messages. A sharded subscription can receive a child's `quote_executed` on another shard, so across shards the two are not ordered. + The children execute together, so either every child is in `executed_rfq_ids` or every child is in `failed_rfq_ids`, except in the rare case that filling one child cancelled a maker order another child needed. Each winning maker receives `batch_quote_executed` for its own children instead. contentType: application/json payload: $ref: '#/components/schemas/rfqBatchExecutedPayload' @@ -2678,9 +2661,9 @@ components: batchQuoteExecuted: name: batch_quote_executed title: Batch Quote Executed - summary: Notification when every child your batch quote won has executed or failed + summary: Notification when the RFQ batch your batch quote won children of has executed or failed description: | - Sent to each winning maker once every child it won has executed or failed. It covers only your children. Each child trade also sends `quote_executed` on the `communications` channel, a separate subscription, so it is not ordered with this message. + Sent to each winning maker once the batch has executed or failed, after the per-child `quote_executed` messages. It covers only your children. A sharded subscription can receive a child's `quote_executed` on another shard, so across shards the two are not ordered. When the RFQ batch creator obscured their ID, `rfq_creator_id` is revealed only if at least one of your children executed. contentType: application/json payload: @@ -2826,7 +2809,7 @@ components: type: string enum: ["", "self"] default: "" - description: 'Communications channel only. Set to "self" to receive RFQ created/deleted events only for RFQs you created. Omitted or empty receives RFQs from all users. Quote notifications are unchanged.' + description: 'Communications channel only. Set to "self" to receive RFQ and RFQ batch created/deleted events only for those you created. Omitted or empty receives them from all users. Quote and batch quote notifications are unchanged.' shard_factor: type: integer description: 'Number of shards for communications channel fanout (optional)' @@ -5048,7 +5031,7 @@ components: format: date-time close_reason: type: string - description: How the batch ended. `executed` when every child executed, `partially executed` when some failed, `execution failed` when none executed. + description: How the batch ended. `executed` when every child executed, `execution failed` when none did, and `partially executed` in the rare case that filling one child cancelled a maker order another child needed. subaccount: type: integer description: | @@ -5097,7 +5080,7 @@ components: format: date-time close_reason: type: string - description: How your children ended. `executed` when every one executed, `partially executed` when some failed, `execution failed` when none executed. + description: How your children ended. `executed` when every one executed, `execution failed` when none did, and `partially executed` in the rare case that filling one child of the batch cancelled a maker order another child needed. subaccount: type: integer description: | diff --git a/specs/openapi.yaml b/specs/openapi.yaml index cdbccbf..3b21ee9 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -1558,6 +1558,8 @@ paths: description: | Transfers funds within the same account. + Subaccount-restricted API keys can only transfer between `event_contract` exchange indices, with `source_subaccount` and `destination_subaccount` both set to the key's subaccount. + When `source_exchange_shard` and `destination_exchange_shard` are the same, Kalshi treats the request as a subaccount transfer. The returned transfer ID appears in the subaccount transfer history. Cross-exchange-index subaccount transfers run in up to three non-atomic steps. If a later step fails, completed steps are not undone, so funds may remain in the primary account on the source or destination exchange index. @@ -1717,7 +1719,7 @@ paths: get: operationId: GetSubaccountBalances summary: Get All Subaccount Balances - description: 'Gets balances for all subaccounts including the primary account.' + description: "Gets balances for all subaccounts including the primary account. For an API key restricted to a subaccount, returns only that subaccount's balances across exchange indices." tags: - portfolio security: @@ -2484,10 +2486,16 @@ paths: round robin. Each child is an ordinary RFQ on one combo market. Makers quote the whole batch, but each child is accepted from whichever maker quotes it best on your side, so the batch may fill from several makers. - Accepting fills every child or none, and each child then executes as its - own trade. Name each child's combo by its market ticker, or by its legs - to look up or create the combo in mve_collection_ticker. Every child is - sized the same way. + Accepting takes every child or none, and the children then execute + together as one fill-or-kill: nothing trades unless every child can + fill in full. In the rare case that filling one child cancels a maker + order another child needed, the batch closes partially executed. Name + each child's combo by its market ticker, or by its legs to look up or + create the combo in mve_collection_ticker. Size each child with its own + contracts_fp or target_cost_dollars, or set one on the batch for every + child that has none; every child must be sized the same way, by + contracts or by target cost. rest_remainder must be false, and FCM + members cannot create RFQ batches yet (403 rfq_batch_unavailable). x-mint: content: | @@ -2796,8 +2804,8 @@ paths: description: >- Confirm the children your batch quote won, all at once. Nothing executes until every winning maker of the batch has confirmed; the last - confirmation starts the execution timer for every child, and each child - executes as its own trade. If any winner misses the confirmation + confirmation starts the execution timer for every child, and the + children then execute together. If any winner misses the confirmation deadline, the whole batch closes without trading. tags: - communications @@ -5132,7 +5140,7 @@ components: MultipleEventTickerQuery: name: event_ticker in: query - description: Event tickers to filter by, as a comma-separated list (maximum 10). + description: Event tickers to filter by, as a comma-separated list (maximum 100). Events with no markets return no orders. schema: type: string x-go-type-skip-optional-pointer: true @@ -8816,7 +8824,7 @@ components: description: Timestamp when the RFQ batch was last updated (visible to the batch creator) close_reason: type: string - description: Why the RFQ batch closed, for example executed, partially executed, or cancelled (visible to the batch creator) + description: Why the RFQ batch closed, for example executed, execution failed, or cancelled. A batch closes partially executed only in the rare case that filling one child cancelled a maker order another child needed (visible to the batch creator) x-go-type-skip-optional-pointer: true client_batch_id: type: string @@ -8855,7 +8863,7 @@ components: CreateRFQBatchChild: type: object - description: One combo to request quotes on. Set exactly one of market_ticker and selected_markets. + description: One combo to request quotes on. Set exactly one of market_ticker and selected_markets, and optionally the child's own size. properties: market_ticker: type: string @@ -8870,6 +8878,14 @@ components: x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: validate: omitempty,max=6,dive + contracts_fp: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + description: Number of contracts requested on this child, in place of the batch's size. Provide at most one of contracts_fp and target_cost_dollars. + target_cost_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: Target cost of this child, in dollars, in place of the batch's size. Provide at most one of contracts_fp and target_cost_dollars. + x-go-type-skip-optional-pointer: true CreateRFQBatchRequest: type: object @@ -8892,18 +8908,18 @@ components: contracts_fp: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: Number of contracts requested on every child. Provide exactly one of contracts_fp and target_cost_dollars. + description: Number of contracts requested on every child that sets no size of its own. Provide at most one of contracts_fp and target_cost_dollars, and one of them unless every child sets its own. target_cost_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Target cost of every child, in dollars. Provide exactly one of contracts_fp and target_cost_dollars. + description: Target cost, in dollars, of every child that sets no size of its own. Provide at most one of contracts_fp and target_cost_dollars, and one of them unless every child sets its own. x-go-type-skip-optional-pointer: true target_cost_excludes_fees: type: boolean - description: Same as on Create RFQ, applied to every child. Only valid together with target_cost_dollars. + description: Same as on Create RFQ, applied to every child. Only valid when the children are sized by target cost. x-go-type-skip-optional-pointer: true rest_remainder: type: boolean - description: Whether to rest the remainder of each child RFQ after execution + description: Must be false. A batch executes as one fill-or-kill, so no child rests a remainder. default: false x-go-type-skip-optional-pointer: true replace_existing: @@ -8925,7 +8941,7 @@ components: x-go-type-skip-optional-pointer: true subtrader_id: type: string - description: The subtrader to create the RFQ batch for (FCM members only) + description: The subtrader to create the RFQ batch for (FCM members only; FCM members cannot create RFQ batches yet) x-go-type-skip-optional-pointer: true subaccount: type: integer @@ -8941,7 +8957,7 @@ components: RFQBatchQuoteStatus: type: string - description: Status of a batch quote. A partially_executed batch quote executed some children and failed others. + description: Status of a batch quote. A partially_executed batch quote executed some children and failed others, which happens only in the rare case that filling one child of the batch cancelled a maker order another child needed. enum: [open, accepted, confirmed, executed, partially_executed, cancelled] RFQBatchQuote: diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index 9095954..da55eb7 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -3104,6 +3104,7 @@ components: - funding_cap - funding_limit_unit - funding_invalid_premium_handling + - halt_sources properties: market_id: type: string @@ -3238,6 +3239,16 @@ components: enum: [percentage, dollar] x-go-type-skip-optional-pointer: true description: Unit for the funding limits configured at market creation. + halt_sources: + type: array + items: + type: string + x-go-type-skip-optional-pointer: true + description: >- + Why the market is halted. A market that has never opened carries + "initial_setup"; one halted for a corporate action carries + "corporate_action". Empty means the market is not halted or its reason + has not been observed, so do not read it as proof the market is live. funding_invalid_premium_handling: type: string enum: [dampen, exclude] diff --git a/tests/_contract_support.py b/tests/_contract_support.py index 7eb9f85..58f5de6 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -2028,6 +2028,15 @@ class Exclusion: # ``paginator_handled`` / ``client_only`` / ``wire_normalization`` entries here # with a required ``reason``. PERPS_EXCLUSIONS: dict[tuple[str, str], Exclusion] = { + # ── perps markets (#557) ── + ("kalshi.perps.models.markets.MarginMarket", "halt_sources"): Exclusion( + reason=( + "spec 3.34.0 adds halt_sources as required; the SDK keeps it optional " + "(None when absent) so payloads/fixtures predating the field still " + "validate and the change ships as a minor release (22.4.0)." + ), + kind="server_omits_despite_required", + ), # ── perps orders (#391) ── ("kalshi.perps.resources.orders.MarginOrdersResource.list_all", "cursor"): Exclusion( reason="paginator-handled; not a caller-facing kwarg on list_all", diff --git a/tests/perps/test_markets.py b/tests/perps/test_markets.py index a34bcb3..0e8e5ca 100644 --- a/tests/perps/test_markets.py +++ b/tests/perps/test_markets.py @@ -51,6 +51,7 @@ def _market_dict(**overrides: object) -> dict[str, object]: "funding_cap": 1.0, "funding_limit_unit": "percentage", "funding_invalid_premium_handling": "dampen", + "halt_sources": [], "leverage_estimate": 2.5, "leverage_estimates": {"1000": 2.5, "10000": 2.0, "100000": 1.5}, "long_leverage_estimates": {"1000": 2.4, "10000": 1.9}, @@ -144,6 +145,7 @@ def test_happy(self, perps_client: PerpsClient) -> None: assert m.funding_cap == Decimal("1.0") assert m.funding_limit_unit == "percentage" assert m.funding_invalid_premium_handling == "dampen" + assert m.halt_sources == [] assert isinstance(m.funding_deadband, Decimal) assert isinstance(m.funding_cap, Decimal) @@ -205,6 +207,19 @@ def test_funding_limit_unit_rejects_unknown(self) -> None: with pytest.raises(ValidationError): MarginMarket.model_validate(_market_dict(funding_limit_unit="bps")) + def test_halt_sources_optional(self) -> None: + payload = _market_dict() + del payload["halt_sources"] + assert MarginMarket.model_validate(payload).halt_sources is None + + def test_halt_sources_parses(self) -> None: + m = MarginMarket.model_validate(_market_dict(halt_sources=["corporate_action"])) + assert m.halt_sources == ["corporate_action"] + + def test_halt_sources_empty_is_valid(self) -> None: + m = MarginMarket.model_validate(_market_dict(halt_sources=[])) + assert m.halt_sources == [] + @respx.mock def test_status_filter(self, perps_client: PerpsClient) -> None: route = respx.get(f"{BASE}/margin/markets").mock( @@ -236,6 +251,7 @@ def test_null_leverage_and_missing_optionals(self, perps_client: PerpsClient) -> "funding_cap": 1.0, "funding_limit_unit": "percentage", "funding_invalid_premium_handling": "dampen", + "halt_sources": ["initial_setup"], "leverage_estimate": None, } ] diff --git a/tests/test_communications.py b/tests/test_communications.py index 0193d28..893cd49 100644 --- a/tests/test_communications.py +++ b/tests/test_communications.py @@ -1907,6 +1907,39 @@ def test_create_rfq_batch_child_forbids_extra(self) -> None: with pytest.raises(ValidationError): CreateRFQBatchChild(market_ticker="MKT-1", phantom=1) # type: ignore[call-arg] + def test_create_rfq_batch_child_serializes_fp_and_dollars(self) -> None: + child = CreateRFQBatchChild( + market_ticker="MKT-1", + contracts=Decimal("10.00"), + target_cost=Decimal("5.00"), + ) + body = child.model_dump(exclude_none=True, by_alias=True, mode="json") + assert body["contracts_fp"] == "10.00" + assert body["target_cost_dollars"] == "5.00" + assert "contracts" not in body + assert "target_cost" not in body + + def test_create_rfq_batch_child_accepts_wire_aliases(self) -> None: + child = CreateRFQBatchChild.model_validate( + { + "market_ticker": "MKT-1", + "contracts_fp": "10.00", + "target_cost_dollars": "5.00", + } + ) + assert child.contracts == Decimal("10.00") + assert child.target_cost == Decimal("5.00") + + def test_create_rfq_batch_child_accepts_python_names(self) -> None: + child = CreateRFQBatchChild(market_ticker="MKT-1", contracts=Decimal("10.00")) + assert child.contracts == Decimal("10.00") + assert child.target_cost is None + + def test_create_rfq_batch_child_omits_optional_size(self) -> None: + child = CreateRFQBatchChild(market_ticker="MKT-1") + body = child.model_dump(exclude_none=True, by_alias=True, mode="json") + assert body == {"market_ticker": "MKT-1"} + def test_create_rfq_batch_quote_request_serializes_bids_without_suffix(self) -> None: req = CreateRFQBatchQuoteRequest( quotes=[ @@ -2068,6 +2101,24 @@ def test_sends_correct_body(self, comms: CommunicationsResource) -> None: "subaccount": 2, } + @respx.mock + def test_sends_per_child_size(self, comms: CommunicationsResource) -> None: + route = respx.post(_BATCH_URL).mock( + return_value=httpx.Response(201, json={"rfq_batch": _MINIMAL_RFQ_BATCH}) + ) + comms.rfq_batches.create( + children=[ + CreateRFQBatchChild(market_ticker="MKT-1", contracts=Decimal("10.00")), + CreateRFQBatchChild(market_ticker="MKT-2", target_cost=Decimal("5.00")), + ], + ) + body = json.loads(route.calls[0].request.content) + assert body["children"][0] == {"market_ticker": "MKT-1", "contracts_fp": "10.00"} + assert body["children"][1] == { + "market_ticker": "MKT-2", + "target_cost_dollars": "5.00", + } + @respx.mock def test_accepts_request_model(self, comms: CommunicationsResource) -> None: route = respx.post(_BATCH_URL).mock( diff --git a/tests/test_contracts.py b/tests/test_contracts.py index c90c8da..05b466e 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -1070,6 +1070,18 @@ def test_ws_schema_coverage(self) -> None: f"but it doesn't exist in the AsyncAPI spec" ) + def test_batch_communications_channel_removed(self) -> None: + """#556: AsyncAPI dropped ``batch_communications`` (16 → 15). + + RFQ-batch notifications now ride ``communications``. The SDK never + shipped a typed subscribe helper for the removed channel, so the + public ``subscribe_communications`` surface stays. + """ + channels = self.spec.get("channels", {}) + assert "batch_communications" not in channels + assert "communications" in channels + assert len(channels) == 15 + def test_ws_envelope_type_drift(self) -> None: """Warn if spec type const values differ from SDK message envelope type defaults.""" schemas = self.spec.get("components", {}).get("schemas", {})