From 4aeb71954f4beb0131d2a49106748340cf996db2 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 22 Oct 2026 10:07:02 -0500 Subject: [PATCH] feat(communications): add RFQ batch endpoints (communications.rfq_batches); release 22.3.0 Closes #553 --- CHANGELOG.md | 32 + CLAUDE.md | 4 +- README.md | 2 +- docs/index.md | 2 +- docs/reference.md | 6 + docs/resources/communications.md | 81 + kalshi/__init__.py | 48 +- kalshi/_contract_map.py | 46 + kalshi/models/__init__.py | 38 + kalshi/models/communications.py | 250 +++ kalshi/resources/communications.py | 717 ++++++++ kalshi/ws/models/communications.py | 2 + pyproject.toml | 2 +- specs/asyncapi.yaml | 897 +++++++++- specs/openapi.yaml | 2669 ++++++++++++++++++---------- specs/perps_scm_openapi.yaml | 4 +- tests/_contract_support.py | 68 + tests/test_async_client.py | 2 +- tests/test_communications.py | 747 ++++++++ tests/test_contracts.py | 9 + tests/test_model_extra_policy.py | 12 +- tests/ws/test_models.py | 17 + 22 files changed, 4731 insertions(+), 924 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47bf37f0..42ea44e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ All notable changes to kalshi-sdk will be documented in this file. +## 22.3.0 — 2026-10-22 + +Reconciles nightly spec drift (#553). Maps the ten core RFQ batch +endpoints added upstream. + +### Added + +- Communications **`rfq_batches`** (sync + async) — + `client.communications.rfq_batches`. Backs + `/communications/rfq_batches`: `list()` / `list_all()` (cursor + paginated, max 20 per page), `get()`, `create()`, `delete()`, + `accept()`, `best_quotes()`. Nested + **`rfq_batches.quotes`**: `list()` (not paginated), `create()`, + `delete()`, `confirm()`. Auth required. Create sizes every child via + `contracts` (`contracts_fp` on the wire) or `target_cost` + (`target_cost_dollars`). Batch-quote bids use `yes_bid` / `no_bid` + (no `_dollars` suffix); accept `min_bids` use Python name `bid`, + serialized as `bid_dollars`. (Closes #553). + +- Optional **`QuoteExecutedPayload.rfq_batch_id`** — set when the + executed quote is a child of an RFQ batch. + +### Changed + +- Re-vendored `specs/` (OpenAPI 3.34.0 still, plus AsyncAPI and Klear + SCM). Besides RFQ batches the upstream changes are description, + example, vendor-extension, and response-side additive optional + fields (`IntraExchangeInstanceTransfer.client_transfer_id`, perps + `ExchangeStatus` / `MarginMarket` extras, WS `batch_communications` + and `pyth_value` channels). Those additive fields and new WS + channels are not mapped in this release. + ## 22.2.0 — 2026-10-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 586ce2c2..30d17534 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,8 +122,8 @@ tests/ ## API Reference -- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.34.0, 118 operations; 117 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 (15 WebSocket channels; 12 typed `subscribe_*` + escape-hatch) +- 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) - 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/README.md b/README.md index f1999b1a..1c2858ce 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/) -- **Full coverage** of the Kalshi REST API (117 mapped of 118 operations across 19 resources, OpenAPI v3.34.0) and WebSocket API (12 typed `subscribe_*` channels + escape-hatch). +- **Full coverage** of the Kalshi REST API (127 mapped of 128 operations across 19 resources, OpenAPI v3.34.0) and WebSocket API (12 typed `subscribe_*` channels + escape-hatch). - **Perps (margin) API**: standalone `PerpsClient` / `AsyncPerpsClient` + `PerpsWebSocket` for the perpetual-futures exchange (50 REST operations, 6 WS channels), plus a `KlearClient` for the Self-Clearing-Member "Klear" settlement API (26 of 27 operations). See [Perps (margin) trading](#perps-margin-trading). - **FIX protocol**: an async-first FIX engine (FIXT.1.1 / FIX50SP2) for both products — order-entry, drop-copy, market-data, post-trade (prediction), and RFQ (prediction) sessions (plus order-group management over the order-entry session) with typed message models, sequence recovery, and order-book / settlement reassembly. `from kalshi import FixClient` / `MarginFixClient`. See [FIX protocol](#fix-protocol-low-latency-trading). - **V2 event-market orders**: `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` / `cancel_all_v2` plus batched variants on `/portfolio/events/orders/*` — the only order-write surface. diff --git a/docs/index.md b/docs/index.md index e1b42569..4d5ee17b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) prediction markets API. -- **Full REST coverage** — 117 mapped of 118 operations across 19 resources +- **Full REST coverage** — 127 mapped of 128 operations across 19 resources (OpenAPI v3.34.0), every kwarg drift-tested against the spec. - **V2 event-market orders** — new `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` / `cancel_all_v2` family on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` diff --git a/docs/reference.md b/docs/reference.md index 715ac693..edd2c58d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -60,6 +60,12 @@ every exception class. ::: kalshi.models.communications.AcceptBlockTradeProposalRequest +::: kalshi.models.communications.CreateRFQBatchRequest + +::: kalshi.models.communications.CreateRFQBatchQuoteRequest + +::: kalshi.models.communications.AcceptRFQBatchRequest + ::: kalshi.models.multivariate.CreateMarketInMultivariateEventCollectionRequest ::: kalshi.models.order_groups.CreateOrderGroupRequest diff --git a/docs/resources/communications.md b/docs/resources/communications.md index 9a96c953..5a4d1fdb 100644 --- a/docs/resources/communications.md +++ b/docs/resources/communications.md @@ -32,6 +32,16 @@ Auth required throughout. | `block_trade_proposals.list(...)` / `block_trade_proposals.list_all(...)` | `GET /communications/block-trade-proposals` | | `block_trade_proposals.create(...)` | `POST /communications/block-trade-proposals` | | `block_trade_proposals.accept(block_trade_proposal_id, *, subtrader_id=None, subaccount=None)` | `POST /communications/block-trade-proposals/{block_trade_proposal_id}/accept` | +| `rfq_batches.list(...)` / `rfq_batches.list_all(...)` | `GET /communications/rfq_batches` | +| `rfq_batches.get(rfq_batch_id)` | `GET /communications/rfq_batches/{rfq_batch_id}` | +| `rfq_batches.create(...)` | `POST /communications/rfq_batches` | +| `rfq_batches.delete(rfq_batch_id)` | `DELETE /communications/rfq_batches/{rfq_batch_id}` | +| `rfq_batches.accept(rfq_batch_id, *, accepted_side, min_bids)` | `PUT /communications/rfq_batches/{rfq_batch_id}/accept` | +| `rfq_batches.best_quotes(rfq_batch_id)` | `GET /communications/rfq_batches/{rfq_batch_id}/best_quotes` | +| `rfq_batches.quotes.list(rfq_batch_id)` | `GET /communications/rfq_batches/{rfq_batch_id}/quotes` | +| `rfq_batches.quotes.create(rfq_batch_id, ...)` | `POST /communications/rfq_batches/{rfq_batch_id}/quotes` | +| `rfq_batches.quotes.delete(rfq_batch_id, batch_quote_id)` | `DELETE /communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}` | +| `rfq_batches.quotes.confirm(rfq_batch_id, batch_quote_id)` | `PUT /communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}/confirm` | `get_id()` returns your `participant_id` — the value you'll pass as `quote_creator_user_id` / `rfq_creator_user_id` when filtering lists. It stays @@ -211,6 +221,61 @@ client.communications.block_trade_proposals.accept(proposal_id) `_dollars` / `_fp` aliasing applies. `price_centi_cents` and `centicount` must each be `>= 1`. +## RFQ batches (v22.3.0) + +`client.communications.rfq_batches` backs `/communications/rfq_batches` — +request quotes on several combo markets at once. Makers quote the whole +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. +resp = client.communications.rfq_batches.create( + children=[ + {"market_ticker": "COMBO-A"}, + {"market_ticker": "COMBO-B"}, + ], + contracts="10.00", + rest_remainder=True, +) +batch_id = resp.rfq_batch.id + +# Maker: quote every child. +quote = client.communications.rfq_batches.quotes.create( + batch_id, + quotes=[ + {"rfq_id": resp.rfq_batch.rfqs[0].id, "yes_bid": "0.56", "no_bid": "0.44"}, + {"rfq_id": resp.rfq_batch.rfqs[1].id, "yes_bid": "0.50", "no_bid": "0.50"}, + ], + rest_remainder=True, +) + +# Requester: read the best bid on each child (makers are not identified). +best = client.communications.rfq_batches.best_quotes(batch_id) +accepted = client.communications.rfq_batches.accept( + batch_id, + accepted_side="yes", + min_bids=[ + {"rfq_id": q.rfq_id, "bid": q.yes_bid} + for q in best.best_quotes.rfqs + if q.yes_bid is not None + ], +) + +# Winning maker confirms the children it won. +client.communications.rfq_batches.quotes.confirm(batch_id, quote.id) +``` + +`list()` / `list_all()` are cursor-paginated (max 20 batches per page). +`quotes.list()` is not paginated — it returns the batch quotes visible to +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 +`contracts` (`contracts_fp` on the wire) or `target_cost` +(`target_cost_dollars`). + ## Migrating from v2.x | v2.x (deprecated) | v3.0.0 (canonical) | @@ -248,6 +313,14 @@ client.communications.block_trade_proposals.accept(proposal_id) options: heading_level: 3 +::: kalshi.resources.communications.RFQBatchesResource + options: + heading_level: 3 + +::: kalshi.resources.communications.RFQBatchQuotesResource + options: + heading_level: 3 + ::: kalshi.resources.communications.AsyncCommunicationsResource options: heading_level: 3 @@ -263,3 +336,11 @@ client.communications.block_trade_proposals.accept(proposal_id) ::: kalshi.resources.communications.AsyncBlockTradeProposalsResource options: heading_level: 3 + +::: kalshi.resources.communications.AsyncRFQBatchesResource + options: + heading_level: 3 + +::: kalshi.resources.communications.AsyncRFQBatchQuotesResource + options: + heading_level: 3 diff --git a/kalshi/__init__.py b/kalshi/__init__.py index a9eb22ce..5877c09e 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -28,6 +28,8 @@ RFQ, AcceptBlockTradeProposalRequest, AcceptQuoteRequest, + AcceptRFQBatchRequest, + AcceptRFQBatchResponse, AccountApiLimits, AccountApiUsageLevelVolumeGoal, AccountApiUsageLevelVolumeProgress, @@ -68,6 +70,11 @@ CreateOrderV2Response, CreateQuoteRequest, CreateQuoteResponse, + CreateRFQBatchChild, + CreateRFQBatchQuoteRequest, + CreateRFQBatchQuoteResponse, + CreateRFQBatchRequest, + CreateRFQBatchResponse, CreateRFQRequest, CreateRFQResponse, CreateSubaccountRequest, @@ -110,6 +117,10 @@ GetOrderGroupResponse, GetQuoteResponse, GetQuotesResponse, + GetRFQBatchBestQuotesResponse, + GetRFQBatchesResponse, + GetRFQBatchQuotesResponse, + GetRFQBatchResponse, GetRFQResponse, GetRFQsResponse, GetStructuredTargetResponse, @@ -161,6 +172,14 @@ QuoteStatusLiteral, RateLimit, RestingMarginReservationLiteral, + RFQBatch, + RFQBatchAcceptLimit, + RFQBatchBestQuote, + RFQBatchBestQuotes, + RFQBatchQuote, + RFQBatchQuoteEntry, + RfqBatchQuoteStatusLiteral, + RfqBatchStatusLiteral, RfqStatusLiteral, Schedule, ScopeList, @@ -208,9 +227,13 @@ from kalshi.resources.communications import ( AsyncBlockTradeProposalsResource, AsyncQuotesResource, + AsyncRFQBatchesResource, + AsyncRFQBatchQuotesResource, AsyncRFQsResource, BlockTradeProposalsResource, QuotesResource, + RFQBatchesResource, + RFQBatchQuotesResource, RFQsResource, ) from kalshi.types import NullableList, StrictInt @@ -219,6 +242,8 @@ "RFQ", "AcceptBlockTradeProposalRequest", "AcceptQuoteRequest", + "AcceptRFQBatchRequest", + "AcceptRFQBatchResponse", "AccountApiLimits", "AccountApiUsageLevelVolumeGoal", "AccountApiUsageLevelVolumeProgress", @@ -237,6 +262,8 @@ "AsyncKlearClient", "AsyncPerpsClient", "AsyncQuotesResource", + "AsyncRFQBatchQuotesResource", + "AsyncRFQBatchesResource", "AsyncRFQsResource", "AuthRequiredError", "Balance", @@ -267,6 +294,11 @@ "CreateOrderV2Response", "CreateQuoteRequest", "CreateQuoteResponse", + "CreateRFQBatchChild", + "CreateRFQBatchQuoteRequest", + "CreateRFQBatchQuoteResponse", + "CreateRFQBatchRequest", + "CreateRFQBatchResponse", "CreateRFQRequest", "CreateRFQResponse", "CreateSubaccountRequest", @@ -313,6 +345,10 @@ "GetOrderGroupResponse", "GetQuoteResponse", "GetQuotesResponse", + "GetRFQBatchBestQuotesResponse", + "GetRFQBatchQuotesResponse", + "GetRFQBatchResponse", + "GetRFQBatchesResponse", "GetRFQResponse", "GetRFQsResponse", "GetStructuredTargetResponse", @@ -390,9 +426,19 @@ "Quote", "QuoteStatusLiteral", "QuotesResource", + "RFQBatch", + "RFQBatchAcceptLimit", + "RFQBatchBestQuote", + "RFQBatchBestQuotes", + "RFQBatchQuote", + "RFQBatchQuoteEntry", + "RFQBatchQuotesResource", + "RFQBatchesResource", "RFQsResource", "RateLimit", "RestingMarginReservationLiteral", + "RfqBatchQuoteStatusLiteral", + "RfqBatchStatusLiteral", "RfqStatusLiteral", "Schedule", "ScopeList", @@ -431,4 +477,4 @@ "Withdrawal", ] -__version__ = "22.2.0" +__version__ = "22.3.0" diff --git a/kalshi/_contract_map.py b/kalshi/_contract_map.py index eae60ca0..cef3898f 100644 --- a/kalshi/_contract_map.py +++ b/kalshi/_contract_map.py @@ -230,6 +230,52 @@ class ContractEntry: sdk_model="kalshi.models.communications.AcceptBlockTradeProposalRequest", spec_schema="AcceptBlockTradeProposalRequest", ), + # RFQ batches. Response items + request bodies, matching Quote/RFQ and + # CreateQuoteRequest/AcceptQuoteRequest. List/create wrappers omitted. + ContractEntry( + sdk_model="kalshi.models.communications.RFQBatch", + spec_schema="RFQBatch", + ), + ContractEntry( + sdk_model="kalshi.models.communications.RFQBatchQuote", + spec_schema="RFQBatchQuote", + ), + ContractEntry( + sdk_model="kalshi.models.communications.RFQBatchBestQuote", + spec_schema="RFQBatchBestQuote", + notes="bid/contracts fields use short names with _dollars/_fp aliases", + ), + ContractEntry( + sdk_model="kalshi.models.communications.RFQBatchBestQuotes", + spec_schema="RFQBatchBestQuotes", + ), + ContractEntry( + sdk_model="kalshi.models.communications.CreateRFQBatchRequest", + spec_schema="CreateRFQBatchRequest", + notes="Uses serialization_alias for contracts_fp / target_cost_dollars", + ), + ContractEntry( + sdk_model="kalshi.models.communications.CreateRFQBatchQuoteRequest", + spec_schema="CreateRFQBatchQuoteRequest", + ), + ContractEntry( + sdk_model="kalshi.models.communications.AcceptRFQBatchRequest", + spec_schema="AcceptRFQBatchRequest", + notes="bid serializes as bid_dollars", + ), + ContractEntry( + sdk_model="kalshi.models.communications.CreateRFQBatchChild", + spec_schema="CreateRFQBatchChild", + ), + ContractEntry( + sdk_model="kalshi.models.communications.RFQBatchQuoteEntry", + spec_schema="RFQBatchQuoteEntry", + ), + ContractEntry( + sdk_model="kalshi.models.communications.RFQBatchAcceptLimit", + spec_schema="RFQBatchAcceptLimit", + notes="bid serializes as bid_dollars", + ), ContractEntry( sdk_model="kalshi.models.subaccounts.SubaccountBalance", spec_schema="SubaccountBalance", diff --git a/kalshi/models/__init__.py b/kalshi/models/__init__.py index 7dd099d0..2827d48f 100644 --- a/kalshi/models/__init__.py +++ b/kalshi/models/__init__.py @@ -24,15 +24,26 @@ RFQ, AcceptBlockTradeProposalRequest, AcceptQuoteRequest, + AcceptRFQBatchRequest, + AcceptRFQBatchResponse, BlockTradeProposal, CreateQuoteRequest, CreateQuoteResponse, + CreateRFQBatchChild, + CreateRFQBatchQuoteRequest, + CreateRFQBatchQuoteResponse, + CreateRFQBatchRequest, + CreateRFQBatchResponse, CreateRFQRequest, CreateRFQResponse, GetBlockTradeProposalsResponse, GetCommunicationsIDResponse, GetQuoteResponse, GetQuotesResponse, + GetRFQBatchBestQuotesResponse, + GetRFQBatchesResponse, + GetRFQBatchQuotesResponse, + GetRFQBatchResponse, GetRFQResponse, GetRFQsResponse, MveSelectedLeg, @@ -40,6 +51,14 @@ ProposeBlockTradeResponse, Quote, QuoteStatusLiteral, + RFQBatch, + RFQBatchAcceptLimit, + RFQBatchBestQuote, + RFQBatchBestQuotes, + RFQBatchQuote, + RFQBatchQuoteEntry, + RfqBatchQuoteStatusLiteral, + RfqBatchStatusLiteral, RfqStatusLiteral, UserFilterLiteral, ) @@ -210,6 +229,8 @@ "RFQ", "AcceptBlockTradeProposalRequest", "AcceptQuoteRequest", + "AcceptRFQBatchRequest", + "AcceptRFQBatchResponse", "AccountApiLimits", "AccountApiUsageLevelVolumeGoal", "AccountApiUsageLevelVolumeProgress", @@ -250,6 +271,11 @@ "CreateOrderV2Response", "CreateQuoteRequest", "CreateQuoteResponse", + "CreateRFQBatchChild", + "CreateRFQBatchQuoteRequest", + "CreateRFQBatchQuoteResponse", + "CreateRFQBatchRequest", + "CreateRFQBatchResponse", "CreateRFQRequest", "CreateRFQResponse", "CreateSubaccountRequest", @@ -292,6 +318,10 @@ "GetOrderGroupResponse", "GetQuoteResponse", "GetQuotesResponse", + "GetRFQBatchBestQuotesResponse", + "GetRFQBatchQuotesResponse", + "GetRFQBatchResponse", + "GetRFQBatchesResponse", "GetRFQResponse", "GetRFQsResponse", "GetStructuredTargetResponse", @@ -341,8 +371,16 @@ "ProposeBlockTradeResponse", "Quote", "QuoteStatusLiteral", + "RFQBatch", + "RFQBatchAcceptLimit", + "RFQBatchBestQuote", + "RFQBatchBestQuotes", + "RFQBatchQuote", + "RFQBatchQuoteEntry", "RateLimit", "RestingMarginReservationLiteral", + "RfqBatchQuoteStatusLiteral", + "RfqBatchStatusLiteral", "RfqStatusLiteral", "Schedule", "ScopeList", diff --git a/kalshi/models/communications.py b/kalshi/models/communications.py index 1b667acd..f65ba55f 100644 --- a/kalshi/models/communications.py +++ b/kalshi/models/communications.py @@ -6,6 +6,7 @@ from pydantic import AliasChoices, AwareDatetime, BaseModel, Field +from kalshi.models.multivariate import TickerPair from kalshi.types import DollarDecimal, FixedPointCount, StrictInt UserFilterLiteral = Literal["self"] @@ -326,3 +327,252 @@ class AcceptBlockTradeProposalRequest(BaseModel): subaccount: StrictInt | None = Field(default=None, ge=0, le=63) model_config = {"extra": "forbid"} + + +# ── RFQ batches ──────────────────────────────────────────────────────────── + + +RfqBatchStatusLiteral = Literal["open", "accepted", "closed"] +"""RFQ batch status filter for GET /communications/rfq_batches. Spec: RFQBatchStatus.""" + +RfqBatchQuoteStatusLiteral = Literal[ + "open", + "accepted", + "confirmed", + "executed", + "partially_executed", + "cancelled", +] +"""Batch-quote status. Spec: RFQBatchQuoteStatus.""" + + +class RFQBatch(BaseModel): + """An RFQ batch — several child RFQs quoted and accepted as a unit.""" + + id: str + creator_id: str + status: str + created_ts: AwareDatetime + rfqs: list[RFQ] + accepted_ts: AwareDatetime | None = None + closed_ts: AwareDatetime | None = None + updated_ts: AwareDatetime | None = None + close_reason: str | None = None + client_batch_id: str | None = None + creator_user_id: str | None = None + creator_subaccount: int | None = None + + model_config = {"extra": "allow"} + + +class GetRFQBatchResponse(BaseModel): + """Single-RFQ-batch envelope.""" + + rfq_batch: RFQBatch + + model_config = {"extra": "allow"} + + +class GetRFQBatchesResponse(BaseModel): + """Paginated list of RFQ batches.""" + + rfq_batches: list[RFQBatch] + cursor: str | None = None + + model_config = {"extra": "allow"} + + +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. + """ + + market_ticker: str | None = None + selected_markets: list[TickerPair] | None = None + + model_config = {"extra": "forbid"} + + +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. + """ + + children: list[CreateRFQBatchChild] + mve_collection_ticker: str | None = None + contracts: FixedPointCount | None = Field( + default=None, + serialization_alias="contracts_fp", + ) + target_cost: DollarDecimal | None = Field( + default=None, + serialization_alias="target_cost_dollars", + ) + target_cost_excludes_fees: bool | None = None + rest_remainder: bool | None = None + replace_existing: bool | None = None + obscure_creator_id: bool | None = None + client_batch_id: str | None = Field(default=None, max_length=64) + subtrader_id: str | None = None + subaccount: StrictInt | None = Field(default=None, ge=0) + + model_config = {"extra": "forbid"} + + +class CreateRFQBatchResponse(BaseModel): + """Wraps the newly-created RFQ batch.""" + + rfq_batch: RFQBatch + + model_config = {"extra": "allow"} + + +class RFQBatchQuote(BaseModel): + """One maker's quote on an RFQ batch.""" + + id: str + rfq_batch_id: str + creator_id: str + rfq_creator_id: str + status: str + created_ts: AwareDatetime + updated_ts: AwareDatetime + quotes: list[Quote] + accepted_side: Literal["yes", "no"] | None = None + accepted_ts: AwareDatetime | None = None + confirmed_ts: AwareDatetime | None = None + rest_remainder: bool | None = None + post_only: bool | None = None + creator_user_id: str | None = None + creator_subaccount: int | None = None + rfq_creator_user_id: str | None = None + rfq_creator_subaccount: int | None = None + + model_config = {"extra": "allow"} + + +class GetRFQBatchQuotesResponse(BaseModel): + """Batch quotes on an RFQ batch visible to the caller. Not paginated.""" + + batch_quotes: list[RFQBatchQuote] + + model_config = {"extra": "allow"} + + +class RFQBatchQuoteEntry(BaseModel): + """Price for one child RFQ inside a batch quote. + + Spec wire uses ``yes_bid`` / ``no_bid`` (no ``_dollars`` suffix), matching + :class:`CreateQuoteRequest`. + """ + + rfq_id: str + yes_bid: DollarDecimal + no_bid: DollarDecimal + + model_config = {"extra": "forbid"} + + +class CreateRFQBatchQuoteRequest(BaseModel): + """Body for POST /communications/rfq_batches/{rfq_batch_id}/quotes.""" + + quotes: list[RFQBatchQuoteEntry] + rest_remainder: bool + post_only: bool | None = None + subaccount: StrictInt | None = Field(default=None, ge=0) + + model_config = {"extra": "forbid"} + + +class CreateRFQBatchQuoteResponse(BaseModel): + """Wraps the newly-created batch quote's id.""" + + id: str + + model_config = {"extra": "allow"} + + +class RFQBatchBestQuote(BaseModel): + """Best open bid on each side of one child RFQ, across makers.""" + + rfq_id: str + market_ticker: str + yes_bid: DollarDecimal | None = Field( + default=None, + validation_alias=AliasChoices("yes_bid_dollars", "yes_bid"), + ) + no_bid: DollarDecimal | None = Field( + default=None, + validation_alias=AliasChoices("no_bid_dollars", "no_bid"), + ) + yes_contracts_offered: FixedPointCount | None = Field( + default=None, + validation_alias=AliasChoices( + "yes_contracts_offered_fp", + "yes_contracts_offered", + ), + ) + no_contracts_offered: FixedPointCount | None = Field( + default=None, + validation_alias=AliasChoices( + "no_contracts_offered_fp", + "no_contracts_offered", + ), + ) + + model_config = {"extra": "allow", "populate_by_name": True} + + +class RFQBatchBestQuotes(BaseModel): + """Best quotes across makers for every child of an RFQ batch.""" + + rfq_batch_id: str + status: str + version: int + yes_complete: bool + no_complete: bool + rfqs: list[RFQBatchBestQuote] + updated_ts: AwareDatetime | None = None + + model_config = {"extra": "allow"} + + +class GetRFQBatchBestQuotesResponse(BaseModel): + """Envelope for GET .../rfq_batches/{rfq_batch_id}/best_quotes.""" + + best_quotes: RFQBatchBestQuotes + + model_config = {"extra": "allow"} + + +class RFQBatchAcceptLimit(BaseModel): + """Price-protection floor for one child on accept.""" + + rfq_id: str + bid: DollarDecimal = Field(serialization_alias="bid_dollars") + + model_config = {"extra": "forbid"} + + +class AcceptRFQBatchRequest(BaseModel): + """Body for PUT /communications/rfq_batches/{rfq_batch_id}/accept.""" + + accepted_side: Literal["yes", "no"] + min_bids: list[RFQBatchAcceptLimit] + client_order_id: str | None = None + + model_config = {"extra": "forbid"} + + +class AcceptRFQBatchResponse(BaseModel): + """Quotes accepted on every child, in batch order.""" + + accepted_side: Literal["yes", "no"] + quotes: list[Quote] + + model_config = {"extra": "allow"} diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index f1a7f09d..bbbe1b2e 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -12,6 +12,7 @@ from __future__ import annotations +import builtins from collections.abc import AsyncIterator, Iterator from datetime import datetime from decimal import Decimal @@ -25,18 +26,32 @@ RFQ, AcceptBlockTradeProposalRequest, AcceptQuoteRequest, + AcceptRFQBatchRequest, + AcceptRFQBatchResponse, BlockTradeProposal, CreateQuoteRequest, CreateQuoteResponse, + CreateRFQBatchChild, + CreateRFQBatchQuoteRequest, + CreateRFQBatchQuoteResponse, + CreateRFQBatchRequest, + CreateRFQBatchResponse, CreateRFQRequest, CreateRFQResponse, GetCommunicationsIDResponse, GetQuoteResponse, + GetRFQBatchBestQuotesResponse, + GetRFQBatchQuotesResponse, + GetRFQBatchResponse, GetRFQResponse, ProposeBlockTradeRequest, ProposeBlockTradeResponse, Quote, QuoteStatusLiteral, + RFQBatch, + RFQBatchAcceptLimit, + RFQBatchQuoteEntry, + RfqBatchStatusLiteral, RfqStatusLiteral, UserFilterLiteral, ) @@ -321,6 +336,127 @@ def _build_accept_block_trade_proposal_body( return request.model_dump(exclude_none=True, by_alias=True, mode="json") +def _list_rfq_batches_params( + *, + cursor: str | None, + limit: int | None, + subaccount: int | None, + status: RfqBatchStatusLiteral | None, + user_filter: UserFilterLiteral | None, +) -> dict[str, Any]: + limit = _validate_limit(limit, hi=20) + return _params( + cursor=cursor, + limit=limit, + subaccount=subaccount, + status=status, + user_filter=user_filter, + ) + + +def _build_create_rfq_batch_body( + request: CreateRFQBatchRequest | None, + *, + children: builtins.list[CreateRFQBatchChild] | None, + mve_collection_ticker: str | None, + contracts: Decimal | str | float | int | None, + target_cost: Decimal | str | float | int | None, + target_cost_excludes_fees: bool | None, + rest_remainder: bool | None, + replace_existing: bool | None, + obscure_creator_id: bool | None, + client_batch_id: str | None, + subtrader_id: str | None, + subaccount: int | None, +) -> dict[str, Any]: + _check_request_exclusive( + request, + children=children, + mve_collection_ticker=mve_collection_ticker, + contracts=contracts, + target_cost=target_cost, + target_cost_excludes_fees=target_cost_excludes_fees, + rest_remainder=rest_remainder, + replace_existing=replace_existing, + obscure_creator_id=obscure_creator_id, + client_batch_id=client_batch_id, + subtrader_id=subtrader_id, + subaccount=subaccount, + ) + if request is None: + if children is None: + raise TypeError("create() requires `children` (or pass `request=...`)") + request = CreateRFQBatchRequest( + children=children, + mve_collection_ticker=mve_collection_ticker, + contracts=contracts, # type: ignore[arg-type] + target_cost=target_cost, # type: ignore[arg-type] + target_cost_excludes_fees=target_cost_excludes_fees, + rest_remainder=rest_remainder, + replace_existing=replace_existing, + obscure_creator_id=obscure_creator_id, + client_batch_id=client_batch_id, + subtrader_id=subtrader_id, + subaccount=subaccount, + ) + return request.model_dump(exclude_none=True, by_alias=True, mode="json") + + +def _build_create_rfq_batch_quote_body( + request: CreateRFQBatchQuoteRequest | None, + *, + quotes: builtins.list[RFQBatchQuoteEntry] | None, + rest_remainder: bool | None, + post_only: bool | None, + subaccount: int | None, +) -> dict[str, Any]: + _check_request_exclusive( + request, + quotes=quotes, + rest_remainder=rest_remainder, + post_only=post_only, + subaccount=subaccount, + ) + if request is None: + if quotes is None or rest_remainder is None: + raise TypeError( + "create() requires `quotes` and `rest_remainder` (or pass `request=...`)" + ) + request = CreateRFQBatchQuoteRequest( + quotes=quotes, + rest_remainder=rest_remainder, + post_only=post_only, + subaccount=subaccount, + ) + return request.model_dump(exclude_none=True, by_alias=True, mode="json") + + +def _build_accept_rfq_batch_body( + request: AcceptRFQBatchRequest | None, + *, + accepted_side: Literal["yes", "no"] | None, + min_bids: builtins.list[RFQBatchAcceptLimit] | None, + client_order_id: str | None, +) -> dict[str, Any]: + _check_request_exclusive( + request, + accepted_side=accepted_side, + min_bids=min_bids, + client_order_id=client_order_id, + ) + if request is None: + if accepted_side is None or min_bids is None: + raise TypeError( + "accept() requires `accepted_side` and `min_bids` (or pass `request=...`)" + ) + request = AcceptRFQBatchRequest( + accepted_side=accepted_side, + min_bids=min_bids, + client_order_id=client_order_id, + ) + return request.model_dump(exclude_none=True, by_alias=True, mode="json") + + # v3.0.0 deprecation messages shown by ``typing_extensions.deprecated`` at the # call site. Templated so each forwarder names its replacement explicitly. def _rfq_dep(new: str, old: str) -> str: @@ -910,6 +1046,295 @@ def accept( ) +class RFQBatchQuotesResource(SyncResource): + """Sync batch-quote sub-resource — ``client.communications.rfq_batches.quotes``. + + Backs ``/communications/rfq_batches/{rfq_batch_id}/quotes``. + """ + + def list( + self, + rfq_batch_id: str, + *, + extra_headers: dict[str, str] | None = None, + ) -> GetRFQBatchQuotesResponse: + self._require_auth() + data = self._get( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/quotes", + extra_headers=extra_headers, + ) + return GetRFQBatchQuotesResponse.model_validate(data) + + @overload + def create( + self, + rfq_batch_id: str, + *, + request: CreateRFQBatchQuoteRequest, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchQuoteResponse: ... + @overload + def create( + self, + rfq_batch_id: str, + *, + quotes: builtins.list[RFQBatchQuoteEntry], + rest_remainder: bool, + post_only: bool | None = ..., + subaccount: int | None = ..., + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchQuoteResponse: ... + def create( + self, + rfq_batch_id: str, + *, + request: CreateRFQBatchQuoteRequest | None = None, + quotes: builtins.list[RFQBatchQuoteEntry] | None = None, + rest_remainder: bool | None = None, + post_only: bool | None = None, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchQuoteResponse: + self._require_auth() + body = _build_create_rfq_batch_quote_body( + request, + quotes=quotes, + rest_remainder=rest_remainder, + post_only=post_only, + subaccount=subaccount, + ) + data = self._post( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/quotes", + json=body, + extra_headers=extra_headers, + ) + return CreateRFQBatchQuoteResponse.model_validate(data) + + def delete( + self, + rfq_batch_id: str, + batch_quote_id: str, + *, + extra_headers: dict[str, str] | None = None, + ) -> None: + self._require_auth() + self._delete( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}" + f"/quotes/{_seg(batch_quote_id, name='batch_quote_id')}", + extra_headers=extra_headers, + ) + + def confirm( + self, + rfq_batch_id: str, + batch_quote_id: str, + *, + extra_headers: dict[str, str] | None = None, + ) -> None: + self._require_auth() + # json={} forces Content-Type: application/json — demo rejects empty PUTs. + self._put( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}" + f"/quotes/{_seg(batch_quote_id, name='batch_quote_id')}/confirm", + json={}, + extra_headers=extra_headers, + ) + + +class RFQBatchesResource(SyncResource): + """Sync RFQ-batch sub-resource — ``client.communications.rfq_batches``. + + Reached via :attr:`CommunicationsResource.rfq_batches`. Backs + ``/communications/rfq_batches`` endpoints. Batch quotes live on + :attr:`quotes`. + """ + + quotes: RFQBatchQuotesResource + + def __init__(self, transport: SyncTransport) -> None: + super().__init__(transport) + self.quotes = RFQBatchQuotesResource(transport) + + def list( + self, + *, + cursor: str | None = None, + limit: int | None = None, + subaccount: int | None = None, + status: RfqBatchStatusLiteral | None = None, + user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, + ) -> Page[RFQBatch]: + self._require_auth() + params = _list_rfq_batches_params( + cursor=cursor, + limit=limit, + subaccount=subaccount, + status=status, + user_filter=user_filter, + ) + return self._list( + "/communications/rfq_batches", + RFQBatch, + "rfq_batches", + params=params, + extra_headers=extra_headers, + ) + + def list_all( + self, + *, + limit: int | None = None, + subaccount: int | None = None, + status: RfqBatchStatusLiteral | None = None, + user_filter: UserFilterLiteral | None = None, + max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> Iterator[RFQBatch]: + self._require_auth() + _validate_max_pages(max_pages) + params = _list_rfq_batches_params( + cursor=None, + limit=limit, + subaccount=subaccount, + status=status, + user_filter=user_filter, + ) + return self._list_all( + "/communications/rfq_batches", + RFQBatch, + "rfq_batches", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, + ) + + def get( + self, rfq_batch_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQBatchResponse: + self._require_auth() + data = self._get( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}", + extra_headers=extra_headers, + ) + return GetRFQBatchResponse.model_validate(data) + + @overload + def create( + self, *, request: CreateRFQBatchRequest, extra_headers: dict[str, str] | None = None + ) -> CreateRFQBatchResponse: ... + @overload + def create( + self, + *, + children: builtins.list[CreateRFQBatchChild], + mve_collection_ticker: str | None = ..., + contracts: Decimal | str | float | int | None = ..., + target_cost: Decimal | str | float | int | None = ..., + target_cost_excludes_fees: bool | None = ..., + rest_remainder: bool | None = ..., + replace_existing: bool | None = ..., + obscure_creator_id: bool | None = ..., + client_batch_id: str | None = ..., + subtrader_id: str | None = ..., + subaccount: int | None = ..., + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchResponse: ... + def create( + self, + *, + request: CreateRFQBatchRequest | None = None, + children: builtins.list[CreateRFQBatchChild] | None = None, + mve_collection_ticker: str | None = None, + contracts: Decimal | str | float | int | None = None, + target_cost: Decimal | str | float | int | None = None, + target_cost_excludes_fees: bool | None = None, + rest_remainder: bool | None = None, + replace_existing: bool | None = None, + obscure_creator_id: bool | None = None, + client_batch_id: str | None = None, + subtrader_id: str | None = None, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchResponse: + self._require_auth() + body = _build_create_rfq_batch_body( + request, + children=children, + mve_collection_ticker=mve_collection_ticker, + contracts=contracts, + target_cost=target_cost, + target_cost_excludes_fees=target_cost_excludes_fees, + rest_remainder=rest_remainder, + replace_existing=replace_existing, + obscure_creator_id=obscure_creator_id, + client_batch_id=client_batch_id, + subtrader_id=subtrader_id, + subaccount=subaccount, + ) + data = self._post("/communications/rfq_batches", json=body, extra_headers=extra_headers) + return CreateRFQBatchResponse.model_validate(data) + + def delete(self, rfq_batch_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + self._require_auth() + self._delete( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}", + extra_headers=extra_headers, + ) + + @overload + def accept( + self, + rfq_batch_id: str, + *, + request: AcceptRFQBatchRequest, + extra_headers: dict[str, str] | None = None, + ) -> AcceptRFQBatchResponse: ... + @overload + def accept( + self, + rfq_batch_id: str, + *, + accepted_side: Literal["yes", "no"], + min_bids: builtins.list[RFQBatchAcceptLimit], + client_order_id: str | None = ..., + extra_headers: dict[str, str] | None = None, + ) -> AcceptRFQBatchResponse: ... + def accept( + self, + rfq_batch_id: str, + *, + request: AcceptRFQBatchRequest | None = None, + accepted_side: Literal["yes", "no"] | None = None, + min_bids: builtins.list[RFQBatchAcceptLimit] | None = None, + client_order_id: str | None = None, + extra_headers: dict[str, str] | None = None, + ) -> AcceptRFQBatchResponse: + self._require_auth() + body = _build_accept_rfq_batch_body( + request, + accepted_side=accepted_side, + min_bids=min_bids, + client_order_id=client_order_id, + ) + data = self._put( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/accept", + json=body, + extra_headers=extra_headers, + ) + return AcceptRFQBatchResponse.model_validate(data) + + def best_quotes( + self, rfq_batch_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQBatchBestQuotesResponse: + self._require_auth() + data = self._get( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/best_quotes", + extra_headers=extra_headers, + ) + return GetRFQBatchBestQuotesResponse.model_validate(data) + + class CommunicationsResource(SyncResource): """Sync communications / RFQ API. @@ -917,6 +1342,7 @@ class CommunicationsResource(SyncResource): - :attr:`rfqs` — :class:`RFQsResource` — ``/communications/rfqs`` surface. - :attr:`quotes` — :class:`QuotesResource` — ``/communications/quotes`` surface. + - :attr:`rfq_batches` — :class:`RFQBatchesResource` — ``/communications/rfq_batches``. The misc :meth:`get_id` endpoint stays at the top level because it has no sub-noun. The flat ``list_rfqs`` / ``get_rfq`` / ``create_rfq`` / @@ -929,12 +1355,14 @@ class CommunicationsResource(SyncResource): rfqs: RFQsResource quotes: QuotesResource block_trade_proposals: BlockTradeProposalsResource + rfq_batches: RFQBatchesResource def __init__(self, transport: SyncTransport) -> None: super().__init__(transport) self.rfqs = RFQsResource(transport) self.quotes = QuotesResource(transport) self.block_trade_proposals = BlockTradeProposalsResource(transport) + self.rfq_batches = RFQBatchesResource(transport) def get_id(self, *, extra_headers: dict[str, str] | None = None) -> GetCommunicationsIDResponse: self._require_auth() @@ -1728,6 +2156,292 @@ async def accept( ) +class AsyncRFQBatchQuotesResource(AsyncResource): + """Async batch-quote sub-resource — ``client.communications.rfq_batches.quotes``.""" + + async def list( + self, + rfq_batch_id: str, + *, + extra_headers: dict[str, str] | None = None, + ) -> GetRFQBatchQuotesResponse: + self._require_auth() + data = await self._get( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/quotes", + extra_headers=extra_headers, + ) + return GetRFQBatchQuotesResponse.model_validate(data) + + @overload + async def create( + self, + rfq_batch_id: str, + *, + request: CreateRFQBatchQuoteRequest, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchQuoteResponse: ... + @overload + async def create( + self, + rfq_batch_id: str, + *, + quotes: builtins.list[RFQBatchQuoteEntry], + rest_remainder: bool, + post_only: bool | None = ..., + subaccount: int | None = ..., + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchQuoteResponse: ... + async def create( + self, + rfq_batch_id: str, + *, + request: CreateRFQBatchQuoteRequest | None = None, + quotes: builtins.list[RFQBatchQuoteEntry] | None = None, + rest_remainder: bool | None = None, + post_only: bool | None = None, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchQuoteResponse: + self._require_auth() + body = _build_create_rfq_batch_quote_body( + request, + quotes=quotes, + rest_remainder=rest_remainder, + post_only=post_only, + subaccount=subaccount, + ) + data = await self._post( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/quotes", + json=body, + extra_headers=extra_headers, + ) + return CreateRFQBatchQuoteResponse.model_validate(data) + + async def delete( + self, + rfq_batch_id: str, + batch_quote_id: str, + *, + extra_headers: dict[str, str] | None = None, + ) -> None: + self._require_auth() + await self._delete( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}" + f"/quotes/{_seg(batch_quote_id, name='batch_quote_id')}", + extra_headers=extra_headers, + ) + + async def confirm( + self, + rfq_batch_id: str, + batch_quote_id: str, + *, + extra_headers: dict[str, str] | None = None, + ) -> None: + self._require_auth() + # json={} forces Content-Type: application/json — demo rejects empty PUTs. + await self._put( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}" + f"/quotes/{_seg(batch_quote_id, name='batch_quote_id')}/confirm", + json={}, + extra_headers=extra_headers, + ) + + +class AsyncRFQBatchesResource(AsyncResource): + """Async RFQ-batch sub-resource — ``client.communications.rfq_batches``.""" + + quotes: AsyncRFQBatchQuotesResource + + def __init__(self, transport: AsyncTransport) -> None: + super().__init__(transport) + self.quotes = AsyncRFQBatchQuotesResource(transport) + + async def list( + self, + *, + cursor: str | None = None, + limit: int | None = None, + subaccount: int | None = None, + status: RfqBatchStatusLiteral | None = None, + user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, + ) -> Page[RFQBatch]: + self._require_auth() + params = _list_rfq_batches_params( + cursor=cursor, + limit=limit, + subaccount=subaccount, + status=status, + user_filter=user_filter, + ) + return await self._list( + "/communications/rfq_batches", + RFQBatch, + "rfq_batches", + params=params, + extra_headers=extra_headers, + ) + + def list_all( + self, + *, + limit: int | None = None, + subaccount: int | None = None, + status: RfqBatchStatusLiteral | None = None, + user_filter: UserFilterLiteral | None = None, + max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> AsyncIterator[RFQBatch]: + """Returns an async iterator — use ``async for``.""" + self._require_auth() + _validate_max_pages(max_pages) + params = _list_rfq_batches_params( + cursor=None, + limit=limit, + subaccount=subaccount, + status=status, + user_filter=user_filter, + ) + return self._list_all( + "/communications/rfq_batches", + RFQBatch, + "rfq_batches", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, + ) + + async def get( + self, rfq_batch_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQBatchResponse: + self._require_auth() + data = await self._get( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}", + extra_headers=extra_headers, + ) + return GetRFQBatchResponse.model_validate(data) + + @overload + async def create( + self, *, request: CreateRFQBatchRequest, extra_headers: dict[str, str] | None = None + ) -> CreateRFQBatchResponse: ... + @overload + async def create( + self, + *, + children: builtins.list[CreateRFQBatchChild], + mve_collection_ticker: str | None = ..., + contracts: Decimal | str | float | int | None = ..., + target_cost: Decimal | str | float | int | None = ..., + target_cost_excludes_fees: bool | None = ..., + rest_remainder: bool | None = ..., + replace_existing: bool | None = ..., + obscure_creator_id: bool | None = ..., + client_batch_id: str | None = ..., + subtrader_id: str | None = ..., + subaccount: int | None = ..., + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchResponse: ... + async def create( + self, + *, + request: CreateRFQBatchRequest | None = None, + children: builtins.list[CreateRFQBatchChild] | None = None, + mve_collection_ticker: str | None = None, + contracts: Decimal | str | float | int | None = None, + target_cost: Decimal | str | float | int | None = None, + target_cost_excludes_fees: bool | None = None, + rest_remainder: bool | None = None, + replace_existing: bool | None = None, + obscure_creator_id: bool | None = None, + client_batch_id: str | None = None, + subtrader_id: str | None = None, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQBatchResponse: + self._require_auth() + body = _build_create_rfq_batch_body( + request, + children=children, + mve_collection_ticker=mve_collection_ticker, + contracts=contracts, + target_cost=target_cost, + target_cost_excludes_fees=target_cost_excludes_fees, + rest_remainder=rest_remainder, + replace_existing=replace_existing, + obscure_creator_id=obscure_creator_id, + client_batch_id=client_batch_id, + subtrader_id=subtrader_id, + subaccount=subaccount, + ) + data = await self._post( + "/communications/rfq_batches", json=body, extra_headers=extra_headers + ) + return CreateRFQBatchResponse.model_validate(data) + + async def delete( + self, rfq_batch_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + self._require_auth() + await self._delete( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}", + extra_headers=extra_headers, + ) + + @overload + async def accept( + self, + rfq_batch_id: str, + *, + request: AcceptRFQBatchRequest, + extra_headers: dict[str, str] | None = None, + ) -> AcceptRFQBatchResponse: ... + @overload + async def accept( + self, + rfq_batch_id: str, + *, + accepted_side: Literal["yes", "no"], + min_bids: builtins.list[RFQBatchAcceptLimit], + client_order_id: str | None = ..., + extra_headers: dict[str, str] | None = None, + ) -> AcceptRFQBatchResponse: ... + async def accept( + self, + rfq_batch_id: str, + *, + request: AcceptRFQBatchRequest | None = None, + accepted_side: Literal["yes", "no"] | None = None, + min_bids: builtins.list[RFQBatchAcceptLimit] | None = None, + client_order_id: str | None = None, + extra_headers: dict[str, str] | None = None, + ) -> AcceptRFQBatchResponse: + self._require_auth() + body = _build_accept_rfq_batch_body( + request, + accepted_side=accepted_side, + min_bids=min_bids, + client_order_id=client_order_id, + ) + data = await self._put( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/accept", + json=body, + extra_headers=extra_headers, + ) + return AcceptRFQBatchResponse.model_validate(data) + + async def best_quotes( + self, rfq_batch_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQBatchBestQuotesResponse: + self._require_auth() + data = await self._get( + f"/communications/rfq_batches/{_seg(rfq_batch_id, name='rfq_batch_id')}/best_quotes", + extra_headers=extra_headers, + ) + return GetRFQBatchBestQuotesResponse.model_validate(data) + + class AsyncCommunicationsResource(AsyncResource): """Async communications / RFQ API. @@ -1735,6 +2449,7 @@ class AsyncCommunicationsResource(AsyncResource): - :attr:`rfqs` — :class:`AsyncRFQsResource` — ``/communications/rfqs``. - :attr:`quotes` — :class:`AsyncQuotesResource` — ``/communications/quotes``. + - :attr:`rfq_batches` — :class:`AsyncRFQBatchesResource` — ``/communications/rfq_batches``. The flat ``list_rfqs`` / ``get_rfq`` / ``create_rfq`` / ``delete_rfq`` / ``list_all_rfqs`` / ``list_quotes`` / ``get_quote`` / ``create_quote`` / @@ -1746,12 +2461,14 @@ class AsyncCommunicationsResource(AsyncResource): rfqs: AsyncRFQsResource quotes: AsyncQuotesResource block_trade_proposals: AsyncBlockTradeProposalsResource + rfq_batches: AsyncRFQBatchesResource def __init__(self, transport: AsyncTransport) -> None: super().__init__(transport) self.rfqs = AsyncRFQsResource(transport) self.quotes = AsyncQuotesResource(transport) self.block_trade_proposals = AsyncBlockTradeProposalsResource(transport) + self.rfq_batches = AsyncRFQBatchesResource(transport) async def get_id( self, *, extra_headers: dict[str, str] | None = None diff --git a/kalshi/ws/models/communications.py b/kalshi/ws/models/communications.py index 2d367829..fdcebd1b 100644 --- a/kalshi/ws/models/communications.py +++ b/kalshi/ws/models/communications.py @@ -153,6 +153,8 @@ class QuoteExecutedPayload(BaseModel): executed_ts: AwareDatetime # AsyncAPI content (2026-07-27): own subaccount when the quote used one. subaccount: int | None = None + # Child of an RFQ batch: set when this execution belongs to a batch. + rfq_batch_id: str | None = None model_config = {"extra": "allow", "populate_by_name": True} diff --git a/pyproject.toml b/pyproject.toml index b8e35e3c..3ede4f64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "22.2.0" +version = "22.3.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 896ed5c1..bce31dc2 100644 --- a/specs/asyncapi.yaml +++ b/specs/asyncapi.yaml @@ -160,6 +160,13 @@ channels: - Supports `update_subscription` with `add_markets` / `delete_markets` / `get_snapshot` actions - `get_snapshot` returns an `orderbook_snapshot` for the requested `market_tickers` without modifying the subscription + **Matching updates across connections:** + - `clock` is the exchange clock. It is the same on every connection, so it can match a market's updates across redundant connections; `seq` counts messages on one subscription only + - One exchange action can make several deltas, so several deltas can share a `clock`. Every connection sends them in the same order, before any delta for that market with a higher `clock` + - A snapshot includes every change with a `clock` at or below its own; the deltas that follow it on that connection have higher clocks + - While you are subscribed to a market, its clocks never go down outside exchange maintenance, but they skip values. They are not comparable across markets. Keep checking `seq` on each connection for missed messages + - A market's clock may decrease during exchange maintenance, so don't compare clocks from before a maintenance period with clocks after it + **Use case:** Building and maintaining a real-time orderbook messages: orderbookSnapshot: @@ -305,6 +312,41 @@ channels: 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: + $ref: '#/components/messages/rfqBatchDeleted' + batchQuoteCreated: + $ref: '#/components/messages/batchQuoteCreated' + rfqBatchBestQuotes: + $ref: '#/components/messages/rfqBatchBestQuotes' + batchQuoteAccepted: + $ref: '#/components/messages/batchQuoteAccepted' + rfqBatchExecuted: + $ref: '#/components/messages/rfqBatchExecuted' + batchQuoteExecuted: + $ref: '#/components/messages/batchQuoteExecuted' + order_group_updates: address: order_group_updates title: Order Group Updates @@ -490,7 +532,7 @@ channels: address: pyth_value title: Pyth Value Feed description: | - Real-time Pyth price updates for configured underlying tickers. Requires authentication. + Real-time Pyth price and Treasury yield updates for configured underlying tickers. Requires authentication. ## Access and pricing @@ -498,9 +540,9 @@ channels: Connect using an authenticated Kalshi WebSocket session. See [Quick Start: WebSockets](/getting_started/quick_start_websockets#authentication) for API key authentication and request signing. - ## Commodities and underlying tickers + ## Underlying tickers - The reference table below lists the underlyings currently streaming in production: spot metals, metal and commodity indices, an equity index, and FX pairs. Index descriptions note Pyth's publishing schedule (24/7, 24/5, or 23/5); spot metals and FX pairs follow their market sessions. + The reference table below lists supported underlyings: spot metals, metal and commodity indices, an equity index, FX pairs, and Treasury yields. Index descriptions note Pyth's publishing schedule (24/7, 24/5, or 23/5); spot metals, FX pairs, and Treasury yields follow their market sessions. | Pyth feed ID | Pyth symbol | Description | |--------------|---------------------------------|---------------------------------------------------------| @@ -518,6 +560,10 @@ channels: | 3525 | `Commodities.Index.CU/USD` | Copper Index in USD (24/7) | | 3266 | `Commodities.Index.COPPER/USD` | Copper Index in USD (23/5) | | 3292 | `Equity.Index.US30/USD` | US30 (Dow Jones Industrial Average) Index in USD (24/5) | + | 1530 | `InterestRate.US2Y` | US 2-year Treasury yield in percent | + | 1534 | `InterestRate.US5Y` | US 5-year Treasury yield in percent | + | 1527 | `InterestRate.US10Y` | US 10-year Treasury yield in percent | + | 1531 | `InterestRate.US30Y` | US 30-year Treasury yield in percent | | 327 | `FX.EUR/USD` | Euro / US Dollar | | 333 | `FX.GBP/USD` | British Pound / US Dollar | | 315 | `FX.AUD/USD` | Australian Dollar / US Dollar | @@ -535,6 +581,8 @@ channels: Use `underlying_list` to discover recently streamed tickers on your connection. Availability can change, including as futures contracts expire; the table does not guarantee that every feed is streaming in every environment. + Treasury yields use the exact symbols above, without a `/USD` suffix. They are included in `["all"]` subscriptions and can also be requested individually, for example with `underlying_tickers: ["InterestRate.US2Y"]`. These are the yield feeds behind the `KX{2,5,10,30}YRRATE15M` and `KX{2,5,10,30}YRRATEH` markets; `/USD` symbols identify separate bond-price feeds. + ## Requirements - Authentication required @@ -633,7 +681,7 @@ channels: - `sid` identifies the subscription stream; use it for `update_subscription` and `unsubscribe`. - Missing or empty `underlying_tickers` for `subscribe_underlyings` or `unsubscribe_underlyings` returns an `error` with `code: 28` ("Underlying tickers required"). - - `value_usd` is a USD price string formatted to eight decimal places. + - `value_usd` is formatted to eight decimal places. The field name is retained for compatibility: for Treasury yield symbols it contains the yield in percent, so `"4.25000000"` means **4.25%**, not a dollar price or a decimal fraction. Other symbols retain their quoted price units. - `source_ts_ms` is the source timestamp in Unix milliseconds; `received_at` is when Kalshi received the update, also in Unix milliseconds. messages: pythValue: @@ -1014,6 +1062,83 @@ operations: tags: - name: private + receiveRFQBatchCreated: + action: send + title: RFQ Batch Created + summary: Receive RFQ batch created notifications + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/rfqBatchCreated' + tags: + - name: private + + receiveRFQBatchDeleted: + action: send + title: RFQ Batch Deleted + summary: Receive RFQ batch deleted notifications + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/rfqBatchDeleted' + tags: + - name: private + + receiveBatchQuoteCreated: + action: send + title: Batch Quote Created + summary: Receive batch quote created notifications + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/batchQuoteCreated' + tags: + - name: private + + receiveRFQBatchBestQuotes: + action: send + title: RFQ Batch Best Quotes + summary: Receive the best quotes across makers on your RFQ batch + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/rfqBatchBestQuotes' + tags: + - name: private + + receiveBatchQuoteAccepted: + action: send + title: Batch Quote Accepted + summary: Receive batch quote accepted notifications + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/batchQuoteAccepted' + tags: + - name: private + + receiveRFQBatchExecuted: + action: send + title: RFQ Batch Executed + summary: Receive RFQ batch executed notifications + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/rfqBatchExecuted' + tags: + - name: private + + receiveBatchQuoteExecuted: + action: send + title: Batch Quote Executed + summary: Receive batch quote executed notifications + channel: + $ref: '#/channels/batch_communications' + messages: + - $ref: '#/channels/batch_communications/messages/batchQuoteExecuted' + tags: + - name: private + receiveOrderGroupUpdates: action: send title: Order Group Updates @@ -1652,6 +1777,7 @@ components: msg: market_ticker: "FED-23DEC-T3.00" market_id: "9b0f6b43-5b68-4f9f-9f02-9a2d1b8ac1a1" + clock: 4182057 yes_dollars_fp: [["0.0800", "300.00"], ["0.2200", "333.00"]] no_dollars_fp: [["0.5400", "20.00"], ["0.5600", "146.00"]] @@ -1673,6 +1799,7 @@ components: msg: market_ticker: "FED-23DEC-T3.00" market_id: "9b0f6b43-5b68-4f9f-9f02-9a2d1b8ac1a1" + clock: 4182061 price_dollars: "0.9600" delta_fp: "-54.00" side: "yes" @@ -1772,7 +1899,7 @@ components: pythValue: name: pyth_value title: Pyth Value Update - summary: Deduplicated real-time Pyth price for an underlying ticker + summary: Deduplicated real-time Pyth price or Treasury yield for an underlying ticker contentType: application/json payload: $ref: '#/components/schemas/pythValuePayload' @@ -1788,6 +1915,17 @@ components: value_usd: "82.12345000" source_ts_ms: 1710000000100 received_at: 1710000000123 + - name: pythTreasuryYieldUpdate + payload: + type: pyth_value + sending_ts_ms: 1710000000124 + sid: 1 + seq: 43 + msg: + underlying_ticker: "InterestRate.US2Y" + value_usd: "4.25000000" + source_ts_ms: 1710000000100 + received_at: 1710000000123 pythUnderlyingList: name: pyth_value_underlying_list @@ -2318,6 +2456,253 @@ components: executed_ts: "2024-12-01T10:05:00Z" subaccount: 3 + rfqBatchCreated: + name: rfq_batch_created + title: RFQ Batch Created + summary: Notification when an RFQ batch is created + description: | + An RFQ batch requests quotes on several combos together, for example every parlay of a round robin. + Distinct legs are listed once in `legs`; each child RFQ lists the indexes of its legs in `leg_indices`. + Quote it with `POST /communications/rfq_batches/{rfq_batch_id}/quotes`, pricing every child. Each child is filled at the best bid across makers. Its child RFQs are not announced separately. + contentType: application/json + payload: + $ref: '#/components/schemas/rfqBatchCreatedPayload' + examples: + - name: rfqBatchCreatedNotification + summary: RFQ batch created notification + payload: + type: rfq_batch_created + sending_ts_ms: 1669149841234 + sid: 15 + seq: 16 + msg: + id: "rfq_batch_123" + creator_id: "43fc3733603f4830a8c3b2986011a5f78d67b03b473dbfd3a3847abd85b63a51" + created_ts: "2024-12-01T10:00:00Z" + legs: + - event_ticker: "KXEVENTA-24DEC" + market_ticker: "KXEVENTA-24DEC-YES" + side: "yes" + - event_ticker: "KXEVENTB-24DEC" + market_ticker: "KXEVENTB-24DEC-YES" + side: "yes" + - event_ticker: "KXEVENTC-24DEC" + market_ticker: "KXEVENTC-24DEC-YES" + side: "no" + rfqs: + - id: "rfq_456" + market_ticker: "KXMVE-24DEC-COMBO-AB" + event_ticker: "KXMVE-24DEC-EVENT-AB" + target_cost_dollars: "5.0000" + mve_collection_ticker: "KXMVE-24DEC" + leg_indices: [0, 1] + - id: "rfq_457" + market_ticker: "KXMVE-24DEC-COMBO-AC" + event_ticker: "KXMVE-24DEC-EVENT-AC" + target_cost_dollars: "5.0000" + mve_collection_ticker: "KXMVE-24DEC" + leg_indices: [0, 2] + + rfqBatchDeleted: + name: rfq_batch_deleted + title: RFQ Batch Deleted + summary: Notification when an RFQ batch closes + description: | + Sent when an RFQ batch closes, together with all of its child RFQs. When an accepted batch finishes executing, its creator receives `rfq_batch_executed` and each winning maker `batch_quote_executed` instead. + contentType: application/json + payload: + $ref: '#/components/schemas/rfqBatchDeletedPayload' + examples: + - name: rfqBatchDeletedNotification + summary: RFQ batch deleted notification + payload: + type: rfq_batch_deleted + sending_ts_ms: 1669149841234 + sid: 15 + seq: 17 + msg: + id: "rfq_batch_123" + creator_id: "43fc3733603f4830a8c3b2986011a5f78d67b03b473dbfd3a3847abd85b63a51" + rfq_ids: ["rfq_456", "rfq_457"] + deleted_ts: "2024-12-01T10:05:00Z" + + batchQuoteCreated: + name: batch_quote_created + title: Batch Quote Created + summary: Notification when you create a batch quote on an RFQ batch + description: | + Sent only to the maker that created the batch quote. A batch quote prices every child RFQ of the batch and replaces your previous batch quote on it. + It counts toward the batch's best quotes for 30 seconds after you submit it; resend it to keep it eligible. A resend within that time keeps the time priority of every bid it leaves unchanged. + The RFQ batch creator never receives individual batch quotes, only the best bid on each child across makers (`rfq_batch_best_quotes`). + contentType: application/json + payload: + $ref: '#/components/schemas/batchQuoteCreatedPayload' + examples: + - name: batchQuoteCreatedNotification + summary: Batch quote created notification + payload: + type: batch_quote_created + sending_ts_ms: 1669149841234 + sid: 15 + seq: 18 + msg: + batch_quote_id: "batch_quote_789" + rfq_batch_id: "rfq_batch_123" + quote_creator_id: "comm_def456" + created_ts: "2024-12-01T10:02:00Z" + quotes: + - quote_id: "quote_1" + rfq_id: "rfq_456" + market_ticker: "KXMVE-24DEC-COMBO-AB" + event_ticker: "KXMVE-24DEC-EVENT-AB" + yes_bid_dollars: "0.0000" + no_bid_dollars: "0.7500" + no_contracts_offered_fp: "20.00" + rfq_target_cost_dollars: "5.0000" + - quote_id: "quote_2" + rfq_id: "rfq_457" + market_ticker: "KXMVE-24DEC-COMBO-AC" + event_ticker: "KXMVE-24DEC-EVENT-AC" + yes_bid_dollars: "0.0000" + no_bid_dollars: "0.8000" + no_contracts_offered_fp: "25.00" + rfq_target_cost_dollars: "5.0000" + subaccount: 2 + + rfqBatchBestQuotes: + name: rfq_batch_best_quotes + title: RFQ Batch Best Quotes + summary: The best quotes across makers on your RFQ batch + description: | + Sent only to the RFQ batch creator when the best bid on either side of any child changes, including when a batch quote stops counting: at most once every 250 ms per batch, carrying the latest snapshot. + Each message is a full snapshot of the batch: one entry per child RFQ, in request order, with the best yes and no bid across every maker's batch quote. Makers are not identified, and the batch creator never receives an individual maker's batch quote. + A batch quote counts for 30 seconds after its maker last submitted it. Among equal bids the earlier one wins, and a maker that resends an unchanged bid within those 30 seconds keeps its time priority. + `version` increases with every snapshot of a batch; ignore a snapshot whose version is not greater than the last one you applied. + Accepting the batch on a side (`PUT /communications/rfq_batches/{rfq_batch_id}/accept`) fills each child at its best bid on that side, possibly from several makers, and needs a bid on every child (`yes_complete` / `no_complete`). Pass the bids you were shown as `min_bids` to protect your price. Nothing executes until every winning maker has confirmed. + contentType: application/json + payload: + $ref: '#/components/schemas/rfqBatchBestQuotesPayload' + examples: + - name: rfqBatchBestQuotesNotification + summary: RFQ batch best quotes notification + payload: + type: rfq_batch_best_quotes + sending_ts_ms: 1669149841234 + sid: 15 + seq: 20 + msg: + rfq_batch_id: "rfq_batch_123" + client_batch_id: "my_round_robin_1" + version: 7 + updated_ts: "2024-12-01T10:02:30Z" + yes_complete: false + no_complete: true + rfqs: + - rfq_id: "rfq_456" + market_ticker: "KXMVE-24DEC-COMBO-AB" + event_ticker: "KXMVE-24DEC-EVENT-AB" + yes_bid_dollars: "0.2000" + no_bid_dollars: "0.7500" + yes_contracts_offered_fp: "6.25" + no_contracts_offered_fp: "20.00" + rfq_target_cost_dollars: "5.0000" + - rfq_id: "rfq_457" + market_ticker: "KXMVE-24DEC-COMBO-AC" + event_ticker: "KXMVE-24DEC-EVENT-AC" + no_bid_dollars: "0.8000" + no_contracts_offered_fp: "25.00" + rfq_target_cost_dollars: "5.0000" + + batchQuoteAccepted: + name: batch_quote_accepted + title: Batch Quote Accepted + summary: Notification when your batch quote wins children of an accepted RFQ batch + description: | + Sent to each winning maker when the RFQ batch creator accepts the batch on one side. Each child goes to the maker bidding best on that side, so one batch can have several winning makers; `quotes` lists only the children you won. + Confirm them with `PUT /communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}/confirm` within the confirmation window. Nothing executes until every winning maker has confirmed; if any winner does not confirm in time, the whole batch closes without trading. + contentType: application/json + payload: + $ref: '#/components/schemas/batchQuoteAcceptedPayload' + examples: + - name: batchQuoteAcceptedNotification + summary: Batch quote accepted notification + payload: + type: batch_quote_accepted + sending_ts_ms: 1669149841234 + sid: 15 + seq: 19 + msg: + batch_quote_id: "batch_quote_789" + rfq_batch_id: "rfq_batch_123" + quote_creator_id: "comm_def456" + rfq_creator_id: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + accepted_side: "no" + accepted_ts: "2024-12-01T10:03:00Z" + quotes: + - quote_id: "quote_2" + rfq_id: "rfq_457" + market_ticker: "KXMVE-24DEC-COMBO-AC" + yes_bid_dollars: "0.0000" + no_bid_dollars: "0.8000" + no_contracts_offered_fp: "25.00" + rfq_target_cost_dollars: "5.0000" + subaccount: 2 + + rfqBatchExecuted: + name: rfq_batch_executed + title: RFQ Batch Executed + summary: Notification when every child of 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. + contentType: application/json + payload: + $ref: '#/components/schemas/rfqBatchExecutedPayload' + examples: + - name: rfqBatchExecutedNotification + summary: RFQ batch executed notification + payload: + type: rfq_batch_executed + sending_ts_ms: 1669149841234 + sid: 15 + seq: 22 + msg: + rfq_batch_id: "rfq_batch_123" + client_batch_id: "my_round_robin_1" + rfq_creator_id: "43fc3733603f4830a8c3b2986011a5f78d67b03b473dbfd3a3847abd85b63a51" + executed_rfq_ids: ["rfq_456", "rfq_457"] + failed_rfq_ids: [] + executed_ts: "2024-12-01T10:03:05Z" + close_reason: "executed" + + batchQuoteExecuted: + name: batch_quote_executed + title: Batch Quote Executed + summary: Notification when every child your batch quote won 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. + 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: + $ref: '#/components/schemas/batchQuoteExecutedPayload' + examples: + - name: batchQuoteExecutedNotification + summary: Batch quote executed notification + payload: + type: batch_quote_executed + sending_ts_ms: 1669149841234 + sid: 15 + seq: 23 + msg: + batch_quote_id: "batch_quote_789" + rfq_batch_id: "rfq_batch_123" + rfq_creator_id: "43fc3733603f4830a8c3b2986011a5f78d67b03b473dbfd3a3847abd85b63a51" + executed_rfq_ids: ["rfq_457"] + failed_rfq_ids: [] + executed_ts: "2024-12-01T10:03:05Z" + close_reason: "executed" + subaccount: 2 + schemas: sendingTimestampMs: type: integer @@ -2985,7 +3370,7 @@ components: description: Qualified Pyth underlying ticker value_usd: type: string - description: USD value formatted to 8 decimal places + description: 'Value formatted to 8 decimal places in the underlying''s quoted units. For Treasury yields (InterestRate.US2Y, InterestRate.US5Y, InterestRate.US10Y, InterestRate.US30Y), this is percent: "4.25000000" means 4.25%. The field name is retained for compatibility.' source_ts_ms: type: integer description: Pyth source timestamp (unix ms) @@ -3036,12 +3421,19 @@ components: $ref: '#/components/schemas/sequenceNumber' msg: type: object - required: ["market_ticker", "market_id"] + required: ["market_ticker", "market_id", "clock"] properties: market_ticker: $ref: '#/components/schemas/marketTicker' market_id: $ref: '#/components/schemas/marketId' + clock: + type: integer + format: int64 + minimum: 0 + description: | + Exchange clock of the newest change this snapshot includes, or 0 if the market has no recorded change. + The snapshot includes every change to this market with a clock at or below this one, and none above it. yes_dollars_fp: type: array description: | @@ -3082,12 +3474,19 @@ components: $ref: '#/components/schemas/sequenceNumber' msg: type: object - required: ["market_ticker", "market_id", "price_dollars", "delta_fp", "side"] + required: ["market_ticker", "market_id", "clock", "price_dollars", "delta_fp", "side"] properties: market_ticker: $ref: '#/components/schemas/marketTicker' market_id: $ref: '#/components/schemas/marketId' + clock: + type: integer + format: int64 + minimum: 1 + description: | + Exchange clock of the exchange action that made this change. Every connection sends the same clock for the same change. + Several deltas can share a clock when one action makes several changes. price_dollars: type: string description: Price level in dollars @@ -3197,7 +3596,7 @@ components: time: type: string deprecated: true - description: Deprecated - Timestamp for when the update happened (RFC3339). Use ts_ms instead. + description: Deprecated - Timestamp for when the update happened (RFC3339, truncated to milliseconds). Use ts_ms instead. format: date-time sending_ts_ms: $ref: '#/components/schemas/sendingTimestampMs' @@ -4223,10 +4622,490 @@ components: description: | Optional - Present only when your side of this quote used a subaccount. Contains your own subaccount number; the counterparty's subaccount is never shared. + rfq_batch_id: + type: string + description: Identifier of the RFQ batch, when the quote was accepted as part of a batch quote. Omitted otherwise. sending_ts_ms: $ref: '#/components/schemas/sendingTimestampMs' + rfqBatchCreatedPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "rfq_batch_created" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["id", "creator_id", "created_ts", "legs", "rfqs"] + properties: + id: + type: string + description: Unique identifier for the RFQ batch + creator_id: + type: string + description: Public communications ID of the batch creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. + created_ts: + type: string + description: Timestamp when the RFQ batch was created + format: date-time + legs: + type: array + description: Distinct legs across the batch's combos, each listed once + items: + type: object + required: ["event_ticker", "market_ticker", "side"] + properties: + event_ticker: + type: string + description: Event ticker for the leg + market_ticker: + type: string + description: Market ticker for the leg + side: + type: string + enum: ["yes", "no"] + description: Side selected for the leg + yes_settlement_value_dollars: + type: string + description: Yes settlement value in dollars for the leg. Omitted when unavailable. + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_option_price_dollars + rfqs: + type: array + description: The child RFQs, in the order they were requested + items: + type: object + required: ["id", "market_ticker", "leg_indices"] + properties: + id: + type: string + description: Unique identifier for the child RFQ + market_ticker: + type: string + description: Combo market ticker of the child RFQ + event_ticker: + type: string + description: Event ticker (optional) + contracts_fp: + type: string + description: Fixed-point contracts requested (2 decimals) (optional) + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + target_cost_dollars: + type: string + description: Target cost in dollars (optional) + x-rust-type: common::types::Value + x-rust-serialize-with: ws::dto::serde_money::serialize_option_value_dollars + mve_collection_ticker: + type: string + description: Multivariate event collection ticker (optional) + leg_indices: + type: array + description: Indexes into legs of this combo's legs + items: + type: integer + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + + rfqBatchDeletedPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "rfq_batch_deleted" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["id", "creator_id", "rfq_ids", "deleted_ts"] + properties: + id: + type: string + description: Unique identifier for the RFQ batch + creator_id: + type: string + description: Public communications ID of the batch creator, obscured as in rfq_batch_created + rfq_ids: + type: array + description: The child RFQs, which closed with the batch + items: + type: string + deleted_ts: + type: string + description: Timestamp when the RFQ batch closed + format: date-time + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + + batchQuoteCreatedPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "batch_quote_created" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["batch_quote_id", "rfq_batch_id", "quote_creator_id", "created_ts", "quotes"] + properties: + batch_quote_id: + type: string + description: Unique identifier for the batch quote + rfq_batch_id: + type: string + description: Identifier of the RFQ batch this batch quote prices + quote_creator_id: + type: string + description: Your public communications ID as the batch quote creator + created_ts: + type: string + description: Timestamp when the batch quote was created + format: date-time + quotes: + type: array + description: One quote per child RFQ + items: + type: object + required: ["quote_id", "rfq_id", "market_ticker", "yes_bid_dollars", "no_bid_dollars"] + properties: + quote_id: + type: string + description: Identifier the child quote keeps if it wins the child + rfq_id: + type: string + description: Identifier of the child RFQ + market_ticker: + type: string + description: Combo market ticker of the child RFQ + event_ticker: + type: string + description: Event ticker (optional) + yes_bid_dollars: + type: string + description: Yes side bid price in dollars + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_price_dollars + no_bid_dollars: + type: string + description: No side bid price in dollars + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_price_dollars + yes_contracts_offered_fp: + type: string + description: Fixed-point yes contracts offered (2 decimals) (optional) + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + no_contracts_offered_fp: + type: string + description: Fixed-point no contracts offered (2 decimals) (optional) + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + rfq_target_cost_dollars: + type: string + description: Target cost from the child RFQ in dollars (optional) + x-rust-type: common::types::Value + x-rust-serialize-with: ws::dto::serde_money::serialize_option_value_dollars + subaccount: + type: integer + description: | + Optional - Present only when your batch quote used a subaccount. + Contains your own subaccount number. + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + + rfqBatchBestQuotesPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "rfq_batch_best_quotes" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["rfq_batch_id", "version", "updated_ts", "yes_complete", "no_complete", "rfqs"] + properties: + rfq_batch_id: + type: string + description: Identifier of your RFQ batch + client_batch_id: + type: string + description: The client_batch_id you gave when creating the batch. Omitted when none was given. + version: + type: integer + format: int64 + minimum: 1 + description: Increases with every snapshot of this batch. Apply a snapshot only if its version is greater than the last one you applied. + updated_ts: + type: string + description: Timestamp when a best bid last changed + format: date-time + yes_complete: + type: boolean + description: Whether every child has a yes bid, so the batch can be accepted on the yes side + no_complete: + type: boolean + description: Whether every child has a no bid, so the batch can be accepted on the no side + rfqs: + type: array + description: One entry per child RFQ, in the order the children were requested + items: + type: object + required: ["rfq_id", "market_ticker"] + properties: + rfq_id: + type: string + description: Identifier of the child RFQ + market_ticker: + type: string + description: Combo market ticker of the child RFQ + event_ticker: + type: string + description: Event ticker (optional) + yes_bid_dollars: + type: string + description: Best yes bid across makers, in dollars. Omitted when no maker bids yes on this child. + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_option_price_dollars + no_bid_dollars: + type: string + description: Best no bid across makers, in dollars. Omitted when no maker bids no on this child. + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_option_price_dollars + contracts_fp: + type: string + description: Fixed-point contracts requested by the child RFQ (2 decimals). Omitted when the child is sized by target cost. + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + yes_contracts_offered_fp: + type: string + description: Fixed-point contracts the best yes bid offers (2 decimals). Omitted when no maker bids yes on this child. + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + no_contracts_offered_fp: + type: string + description: Fixed-point contracts the best no bid offers (2 decimals). Omitted when no maker bids no on this child. + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + rfq_target_cost_dollars: + type: string + description: Target cost from the child RFQ in dollars (optional) + x-rust-type: common::types::Value + x-rust-serialize-with: ws::dto::serde_money::serialize_option_value_dollars + subaccount: + type: integer + description: | + Optional - Present only when your RFQ batch used a subaccount. + Contains your own subaccount number. + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + + batchQuoteAcceptedPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "batch_quote_accepted" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["batch_quote_id", "rfq_batch_id", "quote_creator_id", "rfq_creator_id", "accepted_side", "accepted_ts", "quotes"] + properties: + batch_quote_id: + type: string + description: Identifier of your batch quote. Confirm the children you won with it. + rfq_batch_id: + type: string + description: Identifier of the RFQ batch + quote_creator_id: + type: string + description: Your public communications ID as the batch quote creator + rfq_creator_id: + type: string + description: Public communications ID of the RFQ batch creator, obscured as in quote_accepted + accepted_side: + type: string + description: Which side the RFQ batch creator accepted on every child (yes/no) + enum: ["yes", "no"] + accepted_ts: + type: string + description: Timestamp when the RFQ batch was accepted + format: date-time + quotes: + type: array + description: Only the children you won, each with your accepted quote. Other children went to other makers. + items: + type: object + required: ["quote_id", "rfq_id", "market_ticker", "yes_bid_dollars", "no_bid_dollars"] + properties: + quote_id: + type: string + description: Identifier of the accepted child quote + rfq_id: + type: string + description: Identifier of the child RFQ + market_ticker: + type: string + description: Combo market ticker of the child RFQ + event_ticker: + type: string + description: Event ticker (optional) + yes_bid_dollars: + type: string + description: Yes side bid price in dollars + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_price_dollars + no_bid_dollars: + type: string + description: No side bid price in dollars + x-rust-type: common::types::Price + x-rust-serialize-with: ws::dto::serde_money::serialize_price_dollars + contracts_accepted_fp: + type: string + description: Fixed-point contracts requested by the child RFQ (2 decimals) (optional) + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + yes_contracts_offered_fp: + type: string + description: Fixed-point yes contracts offered (2 decimals) (optional) + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + no_contracts_offered_fp: + type: string + description: Fixed-point no contracts offered (2 decimals) (optional) + x-rust-type: i64 + x-rust-serialize-with: ws::dto::serde_money::serialize_optional_centicount_fixed_point_count + rfq_target_cost_dollars: + type: string + description: Target cost from the child RFQ in dollars (optional) + x-rust-type: common::types::Value + x-rust-serialize-with: ws::dto::serde_money::serialize_option_value_dollars + subaccount: + type: integer + description: | + Optional - Present only when your batch quote used a subaccount. + Contains your own subaccount number; the counterparty's subaccount is never shared. + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + + rfqBatchExecutedPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "rfq_batch_executed" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["rfq_batch_id", "rfq_creator_id", "executed_rfq_ids", "failed_rfq_ids", "executed_ts", "close_reason"] + properties: + rfq_batch_id: + type: string + description: Identifier of your RFQ batch + client_batch_id: + type: string + description: The client_batch_id you gave when creating the batch. Omitted when none was given. + rfq_creator_id: + type: string + description: Your public communications ID as the RFQ batch creator + executed_rfq_ids: + type: array + description: Child RFQs whose trades executed + items: + type: string + failed_rfq_ids: + type: array + description: Child RFQs whose trades failed to execute + items: + type: string + executed_ts: + type: string + description: Timestamp when the last child finished + 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. + subaccount: + type: integer + description: | + Optional - Present only when your RFQ batch used a subaccount. + Contains your own subaccount number. + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + + batchQuoteExecutedPayload: + type: object + required: ["type", "sid", "seq", "msg"] + properties: + type: + type: string + const: "batch_quote_executed" + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: ["batch_quote_id", "rfq_batch_id", "rfq_creator_id", "executed_rfq_ids", "failed_rfq_ids", "executed_ts", "close_reason"] + properties: + batch_quote_id: + type: string + description: Identifier of your batch quote + rfq_batch_id: + type: string + description: Identifier of the RFQ batch + rfq_creator_id: + type: string + description: Public communications ID of the RFQ batch creator. When the creator obscured their ID, it is revealed only if at least one of your children executed. + executed_rfq_ids: + type: array + description: Your children whose trades executed + items: + type: string + failed_rfq_ids: + type: array + description: Your children whose trades failed to execute + items: + type: string + executed_ts: + type: string + description: Timestamp when the last child of the batch finished + 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. + subaccount: + type: integer + description: | + Optional - Present only when your batch quote used a subaccount. + Contains your own subaccount number; the counterparty's subaccount is never shared. + sending_ts_ms: + $ref: '#/components/schemas/sendingTimestampMs' + securitySchemes: apiKey: type: apiKey diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 4f6dea6b..cdbccbf5 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -2417,13 +2417,18 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes: + /communications/rfq_batches: get: - operationId: GetQuotes - summary: Get Quotes + operationId: GetRFQBatches + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Get RFQ Batches description: >- - List quotes. The rfq_id filter requires an RFQ UUID. Pass pagination - cursors back unchanged; malformed RFQ IDs or cursors return HTTP 400. + List RFQ batches, most recently updated first, each with its child RFQs + in request order. A page holds at most limit batches and stops before + its child RFQs would exceed 100; a batch is never split across pages. + Pass pagination cursors back unchanged; a malformed cursor returns + HTTP 400. Use user_filter=self to list only your own batches. tags: - communications security: @@ -2432,84 +2437,36 @@ paths: kalshiAccessTimestamp: [] parameters: - $ref: '#/components/parameters/CursorQuery' - - name: min_ts - in: query - description: Restricts the response to quotes last updated after a timestamp, formatted as a Unix Timestamp - schema: - type: integer - format: int64 - - name: max_ts - in: query - description: Restricts the response to quotes last updated before a timestamp, formatted as a Unix Timestamp - schema: - type: integer - format: int64 + - $ref: '#/components/parameters/SubaccountQuery' - name: limit in: query - description: Parameter to specify the number of results per page. Defaults to 500. + description: Maximum number of batches per page. Defaults to 20. A page also stops before its child RFQs would exceed 100. schema: type: integer format: int32 minimum: 1 - maximum: 500 - default: 500 + maximum: 20 + default: 20 - name: status in: query - description: Filter quotes by status - schema: - type: string - x-go-type-skip-optional-pointer: true - - name: quote_creator_user_id - in: query - description: Filter quotes by quote creator user ID - deprecated: true + description: Filter RFQ batches by status schema: - type: string - x-go-type-skip-optional-pointer: true + $ref: '#/components/schemas/RFQBatchStatus' - name: user_filter in: query required: false - description: Filter for quotes created by the authenticated user. - schema: - $ref: '#/components/schemas/UserFilter' - x-go-type-skip-optional-pointer: true - x-oapi-codegen-extra-tags: - validate: "omitempty,oneof=self" - - name: rfq_user_filter - in: query - required: false - description: Filter for quotes responding to RFQs created by the authenticated user. schema: $ref: '#/components/schemas/UserFilter' x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: validate: "omitempty,oneof=self" - - name: rfq_creator_user_id - in: query - description: Filter quotes by RFQ creator user ID - deprecated: true - schema: - type: string - x-go-type-skip-optional-pointer: true - - name: rfq_creator_subtrader_id - in: query - description: Filter quotes by RFQ creator subtrader ID (FCM members only) - schema: - type: string - x-go-type-skip-optional-pointer: true - - name: rfq_id - in: query - description: Filter quotes by RFQ UUID. Pass the RFQ ID unchanged; malformed IDs return HTTP 400. - schema: - type: string - x-go-type-skip-optional-pointer: true responses: '200': - description: Quotes retrieved successfully + description: RFQ batches retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetQuotesResponse' + $ref: '#/components/schemas/GetRFQBatchesResponse' '400': $ref: '#/components/responses/BadRequestError' '401': @@ -2518,13 +2475,28 @@ paths: $ref: '#/components/responses/InternalServerError' post: - operationId: CreateQuote - summary: Create Quote - description: ' Endpoint for creating a quote in response to an RFQ' + operationId: CreateRFQBatch + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Create RFQ Batch + description: >- + Request quotes on several combos at once, for example every parlay of a + 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. x-mint: content: | - **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + A batch has 2 to 57 child RFQs drawn from at most 6 distinct legs, all + combos on one exchange index. You can have at most 2 open RFQ + batches (a batch being confirmed or executed no longer counts); with + replace_existing, creating a batch closes your oldest open batch + instead of failing. + RFQ batches and single RFQs do not count against each other's limits. tags: - communications @@ -2537,36 +2509,32 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateQuoteRequest' + $ref: '#/components/schemas/CreateRFQBatchRequest' responses: '201': - description: Quote created successfully + description: RFQ batch created successfully content: application/json: schema: - $ref: '#/components/schemas/CreateQuoteResponse' + $ref: '#/components/schemas/CreateRFQBatchResponse' '400': $ref: '#/components/responses/BadRequestError' '401': $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '409': + $ref: '#/components/responses/ConflictError' '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}: + /communications/rfq_batches/{rfq_batch_id}: get: - operationId: GetQuote - summary: Get Quote - deprecated: true - description: 'DEPRECATED: Use GET /communications/rfqs/{rfq_id}/quotes/{quote_id} instead. Endpoint for getting a particular quote.' - x-mint: - content: | - - This endpoint is deprecated. Use `GET /communications/rfqs/{rfq_id}/quotes/{quote_id}` instead. - - - - **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - + operationId: GetRFQBatch + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Get RFQ Batch + description: ' Endpoint for getting a single RFQ batch, with its child RFQs in request order.' tags: - communications security: @@ -2574,14 +2542,14 @@ paths: kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/QuoteIdPath' + - $ref: '#/components/parameters/RfqBatchIdPath' responses: '200': - description: Quote retrieved successfully + description: RFQ batch retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetQuoteResponse' + $ref: '#/components/schemas/GetRFQBatchResponse' '400': $ref: '#/components/responses/BadRequestError' '401': @@ -2592,19 +2560,14 @@ paths: $ref: '#/components/responses/InternalServerError' delete: - operationId: DeleteQuote - summary: Delete Quote - deprecated: true - description: 'DEPRECATED: Use DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id} instead. Endpoint for deleting a quote, which means it can no longer be accepted.' - x-mint: - content: | - - This endpoint is deprecated. Use `DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id}` instead. - - - - **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - + operationId: DeleteRFQBatch + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Delete RFQ Batch + description: >- + Close an RFQ batch and all of its child RFQs. Open batch quotes on it + are cancelled. An accepted batch can no longer be closed: it executes + or expires as a unit. tags: - communications security: @@ -2612,10 +2575,10 @@ paths: kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/QuoteIdPath' + - $ref: '#/components/parameters/RfqBatchIdPath' responses: '204': - description: Quote deleted successfully + description: RFQ batch deleted successfully '400': $ref: '#/components/responses/BadRequestError' '401': @@ -2625,17 +2588,17 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}/accept: - put: - operationId: AcceptQuote - summary: Accept Quote - deprecated: true - description: 'DEPRECATED: Use PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept instead. Endpoint for accepting a quote. This will require the quoter to confirm.' - x-mint: - content: | - - This endpoint is deprecated. Use `PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept` instead. - + /communications/rfq_batches/{rfq_batch_id}/best_quotes: + get: + operationId: GetRFQBatchBestQuotes + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Get RFQ Batch Best Quotes + description: >- + Get the best open bid on each side of every child of your RFQ batch, + across makers. Accepting the batch fills at these prices. Makers are not + identified. Only the batch creator can read the best quotes, and a batch + that is no longer open has none. tags: - communications security: @@ -2643,40 +2606,41 @@ paths: kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/QuoteIdPath' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AcceptQuoteRequest' + - $ref: '#/components/parameters/RfqBatchIdPath' responses: - '204': - description: Quote accepted successfully + '200': + description: Best quotes retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetRFQBatchBestQuotesResponse' '400': $ref: '#/components/responses/BadRequestError' '401': $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}/confirm: + /communications/rfq_batches/{rfq_batch_id}/accept: put: - operationId: ConfirmQuote - summary: Confirm Quote - deprecated: true - description: 'DEPRECATED: Use PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm instead. Endpoint for confirming a quote. This will start a timer for order execution.' - x-mint: - content: | - - This endpoint is deprecated. Use `PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm` instead. - - - - Rate limits are more favorable when providing the RFQ ID. - + operationId: AcceptRFQBatch + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Accept RFQ Batch + description: >- + Accept every child RFQ of your RFQ batch on one side, each from the + maker quoting it best on that side. Either every child is accepted or + none is, and your balance must cover all of them. min_bids protects your + price: give every child the best bid you were shown. If a child's best + bid is now worse, the accept fails with rfq_batch_quotes_changed and + lists that child; a better bid fills at the better price. Each winning + maker then confirms its children. They execute once every winner has + confirmed, and a missed confirmation closes the whole batch. An accepted + batch cannot be cancelled. tags: - communications security: @@ -2684,282 +2648,729 @@ paths: kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/QuoteIdPath' + - $ref: '#/components/parameters/RfqBatchIdPath' requestBody: - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/EmptyResponse' + $ref: '#/components/schemas/AcceptRFQBatchRequest' responses: - '204': - description: Quote confirmed successfully + '200': + description: RFQ batch accepted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AcceptRFQBatchResponse' '400': $ref: '#/components/responses/BadRequestError' '401': $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' + '409': + $ref: '#/components/responses/ConflictError' '500': $ref: '#/components/responses/InternalServerError' - # Multivariate Event Collections endpoints - /api_keys: + /communications/rfq_batches/{rfq_batch_id}/quotes: get: - operationId: GetApiKeys - summary: Get API Keys - description: ' Endpoint for retrieving all API keys associated with the authenticated user. API keys allow programmatic access to the platform without requiring username/password authentication. Each key has a unique identifier and name.' + operationId: GetRFQBatchQuotes + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Get RFQ Batch Quotes + description: >- + List the batch quotes on an RFQ batch that are visible to you. A maker + sees its own batch quote; once the batch is accepted, it lists only the + children that maker won. Once the batch is accepted, the batch creator + sees each winning maker's batch quote on the children it won. While the + batch is open, the creator reads the best quotes across makers with Get + RFQ Batch Best Quotes instead. tags: - - api-keys + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - name: fcm_subtrader_id - in: query - required: false - description: 'Return only API keys bound to this FCM subtrader. Spelled {your_user_id}_{suffix}; only FCM members hold bound keys. Omit to return every key.' - schema: - type: string + - $ref: '#/components/parameters/RfqBatchIdPath' responses: '200': - description: List of API keys retrieved successfully + description: Batch quotes retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetApiKeysResponse' + $ref: '#/components/schemas/GetRFQBatchQuotesResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' post: - operationId: CreateApiKey - summary: Create API Key - description: ' Endpoint for creating a new API key with a user-provided public key. This endpoint allows users with Premier or Market Maker API usage levels to create API keys by providing their own RSA or Ed25519 public key in PEM format. The platform will use this public key to verify signatures on API requests: RSA-PSS with SHA-256 for RSA keys, Ed25519 for Ed25519 keys.' + operationId: CreateRFQBatchQuote + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Create RFQ Batch Quote + description: >- + Quote every child RFQ of an RFQ batch, each exactly once. Each child is + accepted from the maker bidding best on the requester's side, with ties + going to the earliest bid, so you may win some children and not others; + you then confirm the children you won. A new batch quote replaces your + previous one on the same batch, and a child resent at an unchanged price + keeps its time priority. A batch quote can win children only within 30 + seconds of when you last sent it, so resend it to stay eligible. tags: - - api-keys + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/RfqBatchIdPath' requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateApiKeyRequest' + $ref: '#/components/schemas/CreateRFQBatchQuoteRequest' responses: '201': - description: API key created successfully + description: Batch quote created successfully content: application/json: schema: - $ref: '#/components/schemas/CreateApiKeyResponse' + $ref: '#/components/schemas/CreateRFQBatchQuoteResponse' '400': - description: Bad request - invalid input + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '403': - description: Forbidden - insufficient API usage level, or fcm_subtrader_id from a non-FCM caller + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' '409': - description: Conflict - the bound FCM subtrader is not yet visible in the credential registry; retry after the subtrader create propagates + $ref: '#/components/responses/ConflictError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' - /api_keys/generate: - post: - operationId: GenerateApiKey - summary: Generate API Key - description: ' Endpoint for generating a new API key with an automatically created key pair. This endpoint generates a key pair of the requested key_type (RSA when omitted). The public key is stored on the platform, while the private key is returned to the user and must be stored securely. The private key cannot be retrieved again.' + /communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}: + delete: + operationId: DeleteRFQBatchQuote + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Delete RFQ Batch Quote + description: ' Endpoint for withdrawing one of your open batch quotes, which means it can no longer be accepted.' tags: - - api-keys + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/GenerateApiKeyRequest' + parameters: + - $ref: '#/components/parameters/RfqBatchIdPath' + - $ref: '#/components/parameters/BatchQuoteIdPath' responses: - '201': - description: API key generated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GenerateApiKeyResponse' + '204': + description: Batch quote deleted successfully '400': - description: Bad request - invalid input + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized - '403': - description: Forbidden - fcm_subtrader_id requires an FCM member caller - '409': - description: Conflict - the bound FCM subtrader is not yet visible in the credential registry; retry after the subtrader create propagates + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' - /api_keys/{api_key}: - delete: - operationId: DeleteApiKey - summary: Delete API Key - description: ' Endpoint for deleting an existing API key. This endpoint permanently deletes an API key. Once deleted, the key can no longer be used for authentication. This action cannot be undone.' + /communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}/confirm: + put: + operationId: ConfirmRFQBatchQuote + # Hidden from the published docs until RFQ batches launch. + x-excluded: true + summary: Confirm RFQ Batch Quote + 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 + deadline, the whole batch closes without trading. tags: - - api-keys + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - name: api_key - in: path - required: true - description: API key ID to delete - schema: - type: string + - $ref: '#/components/parameters/RfqBatchIdPath' + - $ref: '#/components/parameters/BatchQuoteIdPath' + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyResponse' responses: '204': - description: API key successfully deleted + description: Batch quote confirmed successfully '400': - description: Bad request - invalid API key ID + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '404': - description: API key not found + $ref: '#/components/responses/NotFoundError' + '409': + $ref: '#/components/responses/ConflictError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' - /account/limits: + /communications/quotes: get: - operationId: GetAccountApiLimits - summary: Get Account API Limits - description: 'Endpoint to retrieve the authenticated user''s Predictions API usage tier and token-bucket limits. Public Predictions tiers include Basic, Advanced, Expert, Premier, Paragon, Prime, and Prestige.' + operationId: GetQuotes + summary: Get Quotes + description: >- + List quotes. The rfq_id filter requires an RFQ UUID. Pass pagination + cursors back unchanged; malformed RFQ IDs or cursors return HTTP 400. tags: - - account + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/CursorQuery' + - name: min_ts + in: query + description: Restricts the response to quotes last updated after a timestamp, formatted as a Unix Timestamp + schema: + type: integer + format: int64 + - name: max_ts + in: query + description: Restricts the response to quotes last updated before a timestamp, formatted as a Unix Timestamp + schema: + type: integer + format: int64 + - name: limit + in: query + description: Parameter to specify the number of results per page. Defaults to 500. + schema: + type: integer + format: int32 + minimum: 1 + maximum: 500 + default: 500 + - name: status + in: query + description: Filter quotes by status + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: quote_creator_user_id + in: query + description: Filter quotes by quote creator user ID + deprecated: true + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: user_filter + in: query + required: false + description: Filter for quotes created by the authenticated user. + schema: + $ref: '#/components/schemas/UserFilter' + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: "omitempty,oneof=self" + - name: rfq_user_filter + in: query + required: false + description: Filter for quotes responding to RFQs created by the authenticated user. + schema: + $ref: '#/components/schemas/UserFilter' + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: "omitempty,oneof=self" + - name: rfq_creator_user_id + in: query + description: Filter quotes by RFQ creator user ID + deprecated: true + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: rfq_creator_subtrader_id + in: query + description: Filter quotes by RFQ creator subtrader ID (FCM members only) + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: rfq_id + in: query + description: Filter quotes by RFQ UUID. Pass the RFQ ID unchanged; malformed IDs return HTTP 400. + schema: + type: string + x-go-type-skip-optional-pointer: true responses: '200': - description: Account API tier limits retrieved successfully + description: Quotes retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetAccountApiLimitsResponse' + $ref: '#/components/schemas/GetQuotesResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' - /account/api_usage_level/upgrade: post: - operationId: UpgradeAccountApiUsageLevel - summary: Upgrade Account API Usage Level - description: 'Grants a permanent Advanced API usage-level grant. Currently only the Predictions exchange instance is supported. Criteria: at least 1 of the user''s last 100 Predictions orders was created via API. Use Get Account API Limits to inspect the resulting usage tier and grants.' + operationId: CreateQuote + summary: Create Quote + description: ' Endpoint for creating a quote in response to an RFQ' x-mint: content: | - **Rate limit:** 30 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. tags: - - account + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateQuoteRequest' responses: '201': - description: Advanced API usage-level grant created or refreshed successfully + description: Quote created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateQuoteResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized - '403': - description: No API-created order was found in the user's latest 100 Predictions orders - '429': - description: Rate limit exceeded. This endpoint costs 30 tokens and uses the Predictions Write bucket. + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' - /account/api_usage_level/volume_progress: + /communications/quotes/{quote_id}: get: - operationId: GetAccountApiUsageLevelVolumeProgress - summary: Get Account API Usage Level Volume Progress - description: 'Returns the authenticated user''s latest cron-computed trading volume progress toward volume-based API usage tiers for the predictions (event_contract) lane. Volume figures are reported as fixed-point contract counts.' + operationId: GetQuote + summary: Get Quote + deprecated: true + description: 'DEPRECATED: Use GET /communications/rfqs/{rfq_id}/quotes/{quote_id} instead. Endpoint for getting a particular quote.' + x-mint: + content: | + + This endpoint is deprecated. Use `GET /communications/rfqs/{rfq_id}/quotes/{quote_id}` instead. + + + + **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + tags: - - account + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/QuoteIdPath' responses: '200': - description: Account API usage level volume progress retrieved successfully + description: Quote retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetAccountApiUsageLevelVolumeProgressResponse' + $ref: '#/components/schemas/GetQuoteResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized - '500': - description: Internal server error - - /account/endpoint_costs: - get: - operationId: GetAccountEndpointCosts - summary: List Non-Default Endpoint Costs - description: 'Lists API v2 endpoints whose configured token cost differs from the default cost. Endpoints that use the default cost are omitted.' - tags: - - account - responses: - '200': - description: Non-default endpoint costs retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetAccountEndpointCostsResponse' + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' - /search/tags_by_categories: - get: - operationId: GetTagsForSeriesCategories - summary: Get Tags for Series Categories - description: | - Retrieve tags organized by series categories. + delete: + operationId: DeleteQuote + summary: Delete Quote + deprecated: true + description: 'DEPRECATED: Use DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id} instead. Endpoint for deleting a quote, which means it can no longer be accepted.' + x-mint: + content: | + + This endpoint is deprecated. Use `DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id}` instead. + - This endpoint returns a mapping of series categories to their associated tags, which can be used for filtering and search functionality. + + **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + tags: - - search + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/QuoteIdPath' responses: - '200': - description: Tags retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetTagsForSeriesCategoriesResponse' + '204': + description: Quote deleted successfully + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error - - /search/filters_by_sport: - get: - operationId: GetFiltersForSports - summary: Get Filters for Sports - description: | - Retrieve available filters organized by sport. + $ref: '#/components/responses/InternalServerError' - This endpoint returns filtering options available for each sport, including scopes and competitions. It also provides an ordered list of sports for display purposes. - tags: - - search - responses: - '200': - description: Filters retrieved successfully - content: + /communications/quotes/{quote_id}/accept: + put: + operationId: AcceptQuote + summary: Accept Quote + deprecated: true + description: 'DEPRECATED: Use PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept instead. Endpoint for accepting a quote. This will require the quoter to confirm.' + x-mint: + content: | + + This endpoint is deprecated. Use `PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept` instead. + + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/QuoteIdPath' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AcceptQuoteRequest' + responses: + '204': + description: Quote accepted successfully + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + + /communications/quotes/{quote_id}/confirm: + put: + operationId: ConfirmQuote + summary: Confirm Quote + deprecated: true + description: 'DEPRECATED: Use PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm instead. Endpoint for confirming a quote. This will start a timer for order execution.' + x-mint: + content: | + + This endpoint is deprecated. Use `PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm` instead. + + + + Rate limits are more favorable when providing the RFQ ID. + + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/QuoteIdPath' + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyResponse' + responses: + '204': + description: Quote confirmed successfully + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + + # Multivariate Event Collections endpoints + /api_keys: + get: + operationId: GetApiKeys + summary: Get API Keys + description: ' Endpoint for retrieving all API keys associated with the authenticated user. API keys allow programmatic access to the platform without requiring username/password authentication. Each key has a unique identifier and name.' + tags: + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - name: fcm_subtrader_id + in: query + required: false + description: 'Return only API keys bound to this FCM subtrader. Spelled {your_user_id}_{suffix}; only FCM members hold bound keys. Omit to return every key.' + schema: + type: string + responses: + '200': + description: List of API keys retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetApiKeysResponse' + '401': + description: Unauthorized + '500': + description: Internal server error + + post: + operationId: CreateApiKey + summary: Create API Key + description: ' Endpoint for creating a new API key with a user-provided public key. This endpoint allows users with Premier or Market Maker API usage levels to create API keys by providing their own RSA or Ed25519 public key in PEM format. The platform will use this public key to verify signatures on API requests: RSA-PSS with SHA-256 for RSA keys, Ed25519 for Ed25519 keys.' + tags: + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKeyRequest' + responses: + '201': + description: API key created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKeyResponse' + '400': + description: Bad request - invalid input + '401': + description: Unauthorized + '403': + description: Forbidden - insufficient API usage level, or fcm_subtrader_id from a non-FCM caller + '409': + description: Conflict - the bound FCM subtrader is not yet visible in the credential registry; retry after the subtrader create propagates + '500': + description: Internal server error + + /api_keys/generate: + post: + operationId: GenerateApiKey + summary: Generate API Key + description: ' Endpoint for generating a new API key with an automatically created key pair. This endpoint generates a key pair of the requested key_type (RSA when omitted). The public key is stored on the platform, while the private key is returned to the user and must be stored securely. The private key cannot be retrieved again.' + tags: + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateApiKeyRequest' + responses: + '201': + description: API key generated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateApiKeyResponse' + '400': + description: Bad request - invalid input + '401': + description: Unauthorized + '403': + description: Forbidden - fcm_subtrader_id requires an FCM member caller + '409': + description: Conflict - the bound FCM subtrader is not yet visible in the credential registry; retry after the subtrader create propagates + '500': + description: Internal server error + + /api_keys/{api_key}: + delete: + operationId: DeleteApiKey + summary: Delete API Key + description: ' Endpoint for deleting an existing API key. This endpoint permanently deletes an API key. Once deleted, the key can no longer be used for authentication. This action cannot be undone.' + tags: + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - name: api_key + in: path + required: true + description: API key ID to delete + schema: + type: string + responses: + '204': + description: API key successfully deleted + '400': + description: Bad request - invalid API key ID + '401': + description: Unauthorized + '404': + description: API key not found + '500': + description: Internal server error + + /account/limits: + get: + operationId: GetAccountApiLimits + summary: Get Account API Limits + description: 'Endpoint to retrieve the authenticated user''s Predictions API usage tier and token-bucket limits. Public Predictions tiers include Basic, Advanced, Expert, Premier, Paragon, Prime, and Prestige.' + tags: + - account + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + responses: + '200': + description: Account API tier limits retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetAccountApiLimitsResponse' + '401': + description: Unauthorized + '500': + description: Internal server error + + /account/api_usage_level/upgrade: + post: + operationId: UpgradeAccountApiUsageLevel + summary: Upgrade Account API Usage Level + description: 'Grants a permanent Advanced API usage-level grant. Currently only the Predictions exchange instance is supported. Criteria: at least 1 of the user''s last 100 Predictions orders was created via API. Use Get Account API Limits to inspect the resulting usage tier and grants.' + x-mint: + content: | + + **Rate limit:** 30 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + + tags: + - account + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + responses: + '201': + description: Advanced API usage-level grant created or refreshed successfully + '401': + description: Unauthorized + '403': + description: No API-created order was found in the user's latest 100 Predictions orders + '429': + description: Rate limit exceeded. This endpoint costs 30 tokens and uses the Predictions Write bucket. + '500': + description: Internal server error + + /account/api_usage_level/volume_progress: + get: + operationId: GetAccountApiUsageLevelVolumeProgress + summary: Get Account API Usage Level Volume Progress + description: 'Returns the authenticated user''s latest cron-computed trading volume progress toward volume-based API usage tiers for the predictions (event_contract) lane. Volume figures are reported as fixed-point contract counts.' + tags: + - account + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + responses: + '200': + description: Account API usage level volume progress retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetAccountApiUsageLevelVolumeProgressResponse' + '401': + description: Unauthorized + '500': + description: Internal server error + + /account/endpoint_costs: + get: + operationId: GetAccountEndpointCosts + summary: List Non-Default Endpoint Costs + description: 'Lists API v2 endpoints whose configured token cost differs from the default cost. Endpoints that use the default cost are omitted.' + tags: + - account + responses: + '200': + description: Non-default endpoint costs retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetAccountEndpointCostsResponse' + '500': + description: Internal server error + + /search/tags_by_categories: + get: + operationId: GetTagsForSeriesCategories + summary: Get Tags for Series Categories + description: | + Retrieve tags organized by series categories. + + This endpoint returns a mapping of series categories to their associated tags, which can be used for filtering and search functionality. + tags: + - search + responses: + '200': + description: Tags retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetTagsForSeriesCategoriesResponse' + '401': + description: Unauthorized + '500': + description: Internal server error + + /search/filters_by_sport: + get: + operationId: GetFiltersForSports + summary: Get Filters for Sports + description: | + Retrieve available filters organized by sport. + + This endpoint returns filtering options available for each sport, including scopes and competitions. It also provides an ordered list of sports for display purposes. + tags: + - search + responses: + '200': + description: Filters retrieved successfully + content: application/json: schema: $ref: '#/components/schemas/GetFiltersBySportsResponse' @@ -4670,6 +5081,22 @@ components: schema: type: string + RfqBatchIdPath: + name: rfq_batch_id + in: path + required: true + description: RFQ batch UUID returned when the batch was created. Pass it unchanged; malformed IDs return HTTP 400. + schema: + type: string + + BatchQuoteIdPath: + name: batch_quote_id + in: path + required: true + description: Batch quote UUID returned when the batch quote was created. Pass it unchanged; malformed IDs return HTTP 400. + schema: + type: string + MarketTickerQuery: name: market_ticker in: query @@ -7400,949 +7827,1381 @@ components: limit: $ref: '#/components/schemas/FixedPointDollars' - GetFCMSubtraderBlockedCategoriesResponse: + GetFCMSubtraderBlockedCategoriesResponse: + type: object + required: + - categories + properties: + categories: + type: array + description: The event categories blocked for the subtrader, sorted ascending. Empty when no categories are blocked. + items: + type: string + + UpdateFCMSubtraderBlockedCategoriesRequest: + type: object + required: + - subtrader_id + - category + - blocked + properties: + subtrader_id: + type: string + description: The subtrader whose blocked categories should be updated. Must belong to the requesting FCM. + category: + type: string + description: A single event category to add to or remove from the blocked set, 1-100 characters (e.g. "Politics"). + blocked: + type: boolean + description: True adds the category to the blocked set; false removes it. Removing a category that is not blocked is a no-op. + + UpdateFCMSubtraderBlockedCategoriesResponse: + type: object + required: + - categories + properties: + categories: + type: array + description: The subtrader's full resulting blocked set, sorted ascending. + items: + type: string + + EmptyResponse: + type: object + description: An empty response body + + TargetBalanceAllocation: + type: object + required: + - exchange_index + - percent + properties: + exchange_index: + type: integer + minimum: 0 + description: Exchange index that receives this percentage of sweepable balance + percent: + type: integer + minimum: 0 + maximum: 100 + description: Target percentage of sweepable balance for the exchange index + + TargetBalanceAllocationInput: + type: object + required: + - exchange_index + - percent + properties: + exchange_index: + type: integer + minimum: 0 + description: Exchange index that receives this percentage of sweepable balance + x-go-type: '*int' + x-oapi-codegen-extra-tags: + validate: required,gte=0 + percent: + type: integer + minimum: 0 + maximum: 100 + description: Target percentage of sweepable balance for the exchange index + x-go-type: '*int' + x-oapi-codegen-extra-tags: + validate: required,gte=0,lte=100 + + RestingMarginReservation: + type: string + enum: [none, max, sum] + x-enum-varnames: [RestingMarginReservationNone, RestingMarginReservationMax, RestingMarginReservationSum] + description: | + Collateral an automatic rebalance leaves behind for resting orders. `none` reserves no + collateral for resting orders. `max` reserves the + largest single market-side commitment. `sum` reserves the summed margin of every resting order. + + GetTargetBalanceAllocationResponse: + type: object + required: + - allocations + - resting_margin_reservation + properties: + allocations: + type: array + items: + $ref: '#/components/schemas/TargetBalanceAllocation' + resting_margin_reservation: + $ref: '#/components/schemas/RestingMarginReservation' + + SetTargetBalanceAllocationRequest: + type: object + required: + - allocations + properties: + allocations: + type: array + maxItems: 101 + x-oapi-codegen-extra-tags: + validate: max=101,dive + items: + $ref: '#/components/schemas/TargetBalanceAllocationInput' + resting_margin_reservation: + allOf: + - $ref: '#/components/schemas/RestingMarginReservation' + description: Defaults to `sum` when omitted. + x-oapi-codegen-extra-tags: + validate: omitempty,oneof=none max sum + x-go-type-skip-optional-pointer: true + + IntraExchangeInstanceTransferRequest: + type: object + required: + - source + - destination + - amount + properties: + client_transfer_id: + type: string + format: uuid + description: | + Client ID for duplicate prevention. Reuse only to retry the same request: if a transfer from the same source exchange instance and shard was already accepted with this ID, the request returns 409 and creates no new transfer. The returned transfer_id is always generated by Kalshi. + Only supported for primary accounts. Transfers involving numbered subaccounts reject this field. If omitted, each request creates a new transfer. + source: + $ref: '#/components/schemas/ExchangeInstance' + description: The source exchange instance + destination: + $ref: '#/components/schemas/ExchangeInstance' + description: The destination exchange instance + amount: + type: integer + format: int64 + description: The amount to transfer in centicents + source_exchange_shard: + type: integer + minimum: 0 + maximum: 100 + default: 0 + x-go-type-skip-optional-pointer: true + description: Source exchange shard index (default 0) + x-oapi-codegen-extra-tags: + validate: "gte=0,lte=100" + destination_exchange_shard: + type: integer + minimum: 0 + maximum: 100 + default: 0 + x-go-type-skip-optional-pointer: true + description: Destination exchange shard index (default 0) + x-oapi-codegen-extra-tags: + validate: "gte=0,lte=100" + source_subaccount: + type: integer + default: 0 + x-go-type-skip-optional-pointer: true + description: Source subaccount number (default 0 for the primary account). Only supported for event contract to event contract transfers. + x-oapi-codegen-extra-tags: + validate: "gte=0" + destination_subaccount: + type: integer + default: 0 + x-go-type-skip-optional-pointer: true + description: Destination subaccount number (default 0 for the primary account). Only supported for event contract to event contract transfers. + x-oapi-codegen-extra-tags: + validate: "gte=0" + + IntraExchangeInstanceTransferResponse: + type: object + required: + - transfer_id + properties: + transfer_id: + type: string + description: The ID of the transfer that was created + + IntraExchangeInstanceTransferStatus: + type: string + enum: ['pending', 'complete'] + x-enum-varnames: ['IntraExchangeInstanceTransferStatusPending', 'IntraExchangeInstanceTransferStatusComplete'] + description: Transfer status. + + IntraExchangeInstanceTransfer: + type: object + required: + - transfer_id + - source + - destination + - source_exchange_shard + - destination_exchange_shard + - amount + - status + - created_ts + properties: + transfer_id: + type: string + description: Unique transfer id + client_transfer_id: + type: string + x-go-type-skip-optional-pointer: true + description: The client_transfer_id provided when the transfer was created. Omitted if none was provided. + source: + $ref: '#/components/schemas/ExchangeInstance' + description: Source exchange instance + destination: + $ref: '#/components/schemas/ExchangeInstance' + description: Destination exchange instance + source_exchange_shard: + type: integer + description: Source exchange shard index + destination_exchange_shard: + type: integer + description: Destination exchange shard index + amount: + $ref: '#/components/schemas/FixedPointDollars' + description: Transfer amount in dollars + status: + $ref: '#/components/schemas/IntraExchangeInstanceTransferStatus' + created_ts: + type: integer + format: int64 + description: Unix timestamp when the transfer was created + + GetIntraExchangeInstanceTransfersResponse: type: object required: - - categories + - transfers properties: - categories: + transfers: type: array - description: The event categories blocked for the subtrader, sorted ascending. Empty when no categories are blocked. items: - type: string + $ref: '#/components/schemas/IntraExchangeInstanceTransfer' + cursor: + type: string + description: Cursor for the next page of results. Omitted when there are no further pages. - UpdateFCMSubtraderBlockedCategoriesRequest: + GetIntraExchangeInstanceTransferResponse: type: object required: - - subtrader_id - - category - - blocked + - transfer properties: - subtrader_id: - type: string - description: The subtrader whose blocked categories should be updated. Must belong to the requesting FCM. - category: + transfer: + $ref: '#/components/schemas/IntraExchangeInstanceTransfer' + + OrderGroup: + type: object + required: + - id + - is_auto_cancel_enabled + properties: + id: type: string - description: A single event category to add to or remove from the blocked set, 1-100 characters (e.g. "Politics"). - blocked: + description: Unique identifier for the order group + x-go-type-skip-optional-pointer: true + contracts_limit_fp: + $ref: '#/components/schemas/FixedPointCount' + description: String representation of the current maximum contracts allowed over a rolling 15-second window. + x-go-type-skip-optional-pointer: true + is_auto_cancel_enabled: type: boolean - description: True adds the category to the blocked set; false removes it. Removing a category that is not blocked is a no-op. + description: Whether auto-cancel is enabled for this order group + x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true + x-omitempty: false - UpdateFCMSubtraderBlockedCategoriesResponse: + GetOrderGroupsResponse: type: object - required: - - categories properties: - categories: + order_groups: type: array - description: The subtrader's full resulting blocked set, sorted ascending. items: - type: string - - EmptyResponse: - type: object - description: An empty response body + $ref: '#/components/schemas/OrderGroup' + x-go-type-skip-optional-pointer: true - TargetBalanceAllocation: + GetOrderGroupResponse: type: object required: - - exchange_index - - percent + - is_auto_cancel_enabled + - orders properties: + is_auto_cancel_enabled: + type: boolean + description: Whether auto-cancel is enabled for this order group + contracts_limit_fp: + $ref: '#/components/schemas/FixedPointCount' + description: String representation of the current maximum contracts allowed over a rolling 15-second window. + x-go-type-skip-optional-pointer: true + orders: + type: array + items: + type: string + description: List of order IDs that belong to this order group + x-go-type-skip-optional-pointer: true exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true + x-omitempty: false + + CreateOrderGroupRequest: + type: object + properties: + subaccount: type: integer minimum: 0 - description: Exchange index that receives this percentage of sweepable balance - percent: + description: Optional subaccount number to use for this order group (0 for primary, 1-63 for subaccounts). Subaccount-restricted API keys must omit this field or pass their locked subaccount. + contracts_limit: type: integer - minimum: 0 - maximum: 100 - description: Target percentage of sweepable balance for the exchange index + format: int64 + minimum: 1 + description: Specifies the maximum number of contracts that can be matched within this group over a rolling 15-second window. Whole contracts only. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: omitempty,gte=1 + contracts_limit_fp: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + description: String representation of the maximum number of contracts that can be matched within this group over a rolling 15-second window. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + description: Identifier for an exchange shard. Defaults to 0. + x-go-type-skip-optional-pointer: true - TargetBalanceAllocationInput: + UpdateOrderGroupLimitRequest: type: object - required: - - exchange_index - - percent properties: - exchange_index: + contracts_limit: type: integer - minimum: 0 - description: Exchange index that receives this percentage of sweepable balance - x-go-type: '*int' + format: int64 + minimum: 1 + description: New maximum number of contracts that can be matched within this group over a rolling 15-second window. Whole contracts only. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: - validate: required,gte=0 - percent: + validate: omitempty,gte=1 + contracts_limit_fp: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + description: String representation of the new maximum number of contracts that can be matched within this group over a rolling 15-second window. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + + CreateOrderGroupResponse: + type: object + required: + - order_group_id + - subaccount + properties: + order_group_id: + type: string + description: The unique identifier for the created order group + subaccount: type: integer minimum: 0 - maximum: 100 - description: Target percentage of sweepable balance for the exchange index - x-go-type: '*int' - x-oapi-codegen-extra-tags: - validate: required,gte=0,lte=100 + description: Subaccount number that owns the created order group (0 for primary, 1-63 for subaccounts). + x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true + x-omitempty: false - RestingMarginReservation: - type: string - enum: [none, max, sum] - x-enum-varnames: [RestingMarginReservationNone, RestingMarginReservationMax, RestingMarginReservationSum] - description: | - Collateral an automatic rebalance leaves behind for resting orders. `none` reserves no - collateral for resting orders. `max` reserves the - largest single market-side commitment. `sum` reserves the summed margin of every resting order. + GetCommunicationsIDResponse: + type: object + required: + - communications_id + properties: + communications_id: + type: string + description: A public communications ID which is used to identify the user - GetTargetBalanceAllocationResponse: + BlockTradeProposal: type: object required: - - allocations - - resting_margin_reservation + - id + - proposer_user_id + - buyer_user_id + - seller_user_id + - market_ticker + - price_centi_cents + - centicount + - maker_side + - expiration_ts + - status + - created_ts + - updated_ts + - buyer_accepted + - seller_accepted properties: - allocations: - type: array - items: - $ref: '#/components/schemas/TargetBalanceAllocation' - resting_margin_reservation: - $ref: '#/components/schemas/RestingMarginReservation' + id: + type: string + description: Unique identifier for the block trade proposal + proposer_user_id: + type: string + description: User ID of the proposal creator + buyer_user_id: + type: string + description: User ID of the buyer. Empty when the authenticated user is not the buyer. + buyer_subtrader_id: + type: string + description: Subtrader ID of the buyer. Empty when the authenticated user is not the buyer. + x-go-type-skip-optional-pointer: true + seller_user_id: + type: string + description: User ID of the seller. Empty when the authenticated user is not the seller. + seller_subtrader_id: + type: string + description: Subtrader ID of the seller. Empty when the authenticated user is not the seller. + x-go-type-skip-optional-pointer: true + market_ticker: + type: string + description: The ticker of the market for this block trade + price_centi_cents: + type: integer + format: int64 + description: Price in centi-cents + centicount: + type: integer + format: int64 + description: Number of contracts in centicounts + maker_side: + type: string + description: The maker side of the trade + enum: ['yes', 'no'] + expiration_ts: + type: string + format: date-time + description: Expiration time of the proposal + status: + type: string + description: Current status of the proposal + created_ts: + type: string + format: date-time + description: Timestamp when the proposal was created + updated_ts: + type: string + format: date-time + description: Timestamp when the proposal was last updated + buyer_accepted: + type: boolean + description: Whether the buyer has accepted the proposal + seller_accepted: + type: boolean + description: Whether the seller has accepted the proposal + buyer_accepted_ts: + type: string + format: date-time + description: Timestamp when the buyer accepted + seller_accepted_ts: + type: string + format: date-time + description: Timestamp when the seller accepted + executed_ts: + type: string + format: date-time + description: Timestamp when the proposal was executed + buyer_order_id: + type: string + description: Order ID for the buyer after the proposal is executed + x-go-type-skip-optional-pointer: true + seller_order_id: + type: string + description: Order ID for the seller after the proposal is executed + x-go-type-skip-optional-pointer: true - SetTargetBalanceAllocationRequest: + GetBlockTradeProposalsResponse: type: object required: - - allocations + - block_trade_proposals properties: - allocations: + block_trade_proposals: type: array - maxItems: 101 - x-oapi-codegen-extra-tags: - validate: max=101,dive items: - $ref: '#/components/schemas/TargetBalanceAllocationInput' - resting_margin_reservation: - allOf: - - $ref: '#/components/schemas/RestingMarginReservation' - description: Defaults to `sum` when omitted. - x-oapi-codegen-extra-tags: - validate: omitempty,oneof=none max sum + $ref: '#/components/schemas/BlockTradeProposal' + description: List of block trade proposals + cursor: + type: string + description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - IntraExchangeInstanceTransferRequest: + ProposeBlockTradeRequest: type: object required: - - source - - destination - - amount + - buyer_user_id + - seller_user_id + - market_ticker + - price_centi_cents + - centicount + - maker_side + - expiration_ts properties: - client_transfer_id: + buyer_user_id: type: string - format: uuid - description: | - Client ID for duplicate prevention. Reuse only to retry the same request: if a transfer from the same source exchange instance and shard was already accepted with this ID, the request returns 409 and creates no new transfer. The returned transfer_id is always generated by Kalshi. - Only supported for primary accounts. Transfers involving numbered subaccounts reject this field. If omitted, each request creates a new transfer. - source: - $ref: '#/components/schemas/ExchangeInstance' - description: The source exchange instance - destination: - $ref: '#/components/schemas/ExchangeInstance' - description: The destination exchange instance - amount: - type: integer - format: int64 - description: The amount to transfer in centicents - source_exchange_shard: + description: User ID of the buyer + x-oapi-codegen-extra-tags: + validate: required + buyer_subtrader_id: + type: string + description: Subtrader ID of the buyer. Provide either this or buyer_subaccount, not both. + x-go-type-skip-optional-pointer: true + buyer_subaccount: type: integer minimum: 0 - maximum: 100 - default: 0 - x-go-type-skip-optional-pointer: true - description: Source exchange shard index (default 0) + maximum: 63 + description: User-managed subaccount number of the buyer (0 for primary, 1-63 for numbered subaccounts). Provide either this or buyer_subtrader_id, not both. + seller_user_id: + type: string + description: User ID of the seller x-oapi-codegen-extra-tags: - validate: "gte=0,lte=100" - destination_exchange_shard: + validate: required + seller_subtrader_id: + type: string + description: Subtrader ID of the seller. Provide either this or seller_subaccount, not both. + x-go-type-skip-optional-pointer: true + seller_subaccount: type: integer minimum: 0 - maximum: 100 - default: 0 - x-go-type-skip-optional-pointer: true - description: Destination exchange shard index (default 0) + maximum: 63 + description: User-managed subaccount number of the seller (0 for primary, 1-63 for numbered subaccounts). Provide either this or seller_subtrader_id, not both. + market_ticker: + type: string + description: The ticker of the market for this block trade x-oapi-codegen-extra-tags: - validate: "gte=0,lte=100" - source_subaccount: + validate: required + price_centi_cents: type: integer - default: 0 - x-go-type-skip-optional-pointer: true - description: Source subaccount number (default 0 for the primary account). Only supported for event contract to event contract transfers. + format: int64 + minimum: 1 + description: Price in centi-cents x-oapi-codegen-extra-tags: - validate: "gte=0" - destination_subaccount: + validate: required,gt=0 + centicount: type: integer - default: 0 - x-go-type-skip-optional-pointer: true - description: Destination subaccount number (default 0 for the primary account). Only supported for event contract to event contract transfers. + format: int64 + minimum: 1 + description: Number of contracts in centicounts x-oapi-codegen-extra-tags: - validate: "gte=0" + validate: required,gt=0 + maker_side: + type: string + description: The maker side of the trade + enum: ['yes', 'no'] + x-oapi-codegen-extra-tags: + validate: required,oneof=yes no + expiration_ts: + type: string + format: date-time + description: Expiration time of the proposal + x-oapi-codegen-extra-tags: + validate: required - IntraExchangeInstanceTransferResponse: + ProposeBlockTradeResponse: type: object required: - - transfer_id + - block_trade_proposal_id properties: - transfer_id: + block_trade_proposal_id: type: string - description: The ID of the transfer that was created + description: The ID of the newly created block trade proposal - IntraExchangeInstanceTransferStatus: - type: string - enum: ['pending', 'complete'] - x-enum-varnames: ['IntraExchangeInstanceTransferStatusPending', 'IntraExchangeInstanceTransferStatusComplete'] - description: Transfer status. + AcceptBlockTradeProposalRequest: + type: object + properties: + subtrader_id: + type: string + description: Subtrader ID to accept as. Provide either this or subaccount, not both. + x-go-type-skip-optional-pointer: true + subaccount: + type: integer + minimum: 0 + maximum: 63 + description: User-managed subaccount number to accept as (0 for primary, 1-63 for numbered subaccounts). Provide either this or subtrader_id, not both. - IntraExchangeInstanceTransfer: + RFQ: type: object required: - - transfer_id - - source - - destination - - source_exchange_shard - - destination_exchange_shard - - amount + - id + - creator_id + - contracts_fp + - market_ticker - status - created_ts properties: - transfer_id: + id: type: string - description: Unique transfer id - client_transfer_id: + description: UUID of the RFQ. Preserve the exact returned string. + creator_id: type: string - x-go-type-skip-optional-pointer: true - description: The client_transfer_id provided when the transfer was created. Omitted if none was provided. - source: - $ref: '#/components/schemas/ExchangeInstance' - description: Source exchange instance - destination: - $ref: '#/components/schemas/ExchangeInstance' - description: Destination exchange instance - source_exchange_shard: - type: integer - description: Source exchange shard index - destination_exchange_shard: - type: integer - description: Destination exchange shard index - amount: + description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. After successful quote execution, subsequent RFQ responses show the normal public communications ID to all viewers. + market_ticker: + type: string + description: The ticker of the market this RFQ is for + contracts_fp: + $ref: '#/components/schemas/FixedPointCount' + description: String representation of the number of contracts requested in the RFQ + target_cost_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Transfer amount in dollars + description: Total value of the RFQ in dollars + target_cost_excludes_fees: + type: boolean + description: >- + True when the target cost is principal-only and quote sizes are + computed without reserving taker fees (fees are charged on top). + x-go-type-skip-optional-pointer: true status: - $ref: '#/components/schemas/IntraExchangeInstanceTransferStatus' + type: string + description: Current status of the RFQ (open, closed) + enum: [open, closed] created_ts: - type: integer - format: int64 - description: Unix timestamp when the transfer was created - - GetIntraExchangeInstanceTransfersResponse: - type: object - required: - - transfers - properties: - transfers: - type: array - items: - $ref: '#/components/schemas/IntraExchangeInstanceTransfer' - cursor: type: string - description: Cursor for the next page of results. Omitted when there are no further pages. - - GetIntraExchangeInstanceTransferResponse: - type: object - required: - - transfer - properties: - transfer: - $ref: '#/components/schemas/IntraExchangeInstanceTransfer' - - OrderGroup: - type: object - required: - - id - - is_auto_cancel_enabled - properties: - id: + format: date-time + description: Timestamp when the RFQ was created + mve_collection_ticker: type: string - description: Unique identifier for the order group + description: Ticker of the MVE collection this market belongs to x-go-type-skip-optional-pointer: true - contracts_limit_fp: - $ref: '#/components/schemas/FixedPointCount' - description: String representation of the current maximum contracts allowed over a rolling 15-second window. + mve_selected_legs: + type: array + x-omitempty: true + items: + $ref: '#/components/schemas/MveSelectedLeg' + description: Selected legs for the MVE collection x-go-type-skip-optional-pointer: true - is_auto_cancel_enabled: + rest_remainder: type: boolean - description: Whether auto-cancel is enabled for this order group + description: Whether to rest the remainder of the RFQ after execution + cancellation_reason: + type: string + description: Reason for RFQ cancellation if cancelled x-go-type-skip-optional-pointer: true - exchange_index: - allOf: - - $ref: '#/components/schemas/ExchangeIndex' + creator_user_id: + type: string + description: User ID of the RFQ creator (private field) x-go-type-skip-optional-pointer: true - x-omitempty: false + creator_subaccount: + type: integer + description: Subaccount number of the RFQ creator (visible when the caller is the RFQ creator) + cancelled_ts: + type: string + format: date-time + description: Timestamp when the RFQ was cancelled + updated_ts: + type: string + format: date-time + description: Timestamp when the RFQ was last updated - GetOrderGroupsResponse: + GetRFQsResponse: type: object + required: + - rfqs properties: - order_groups: + rfqs: type: array items: - $ref: '#/components/schemas/OrderGroup' + $ref: '#/components/schemas/RFQ' + description: List of RFQs matching the query criteria + cursor: + type: string + description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - GetOrderGroupResponse: + GetRFQResponse: type: object required: - - is_auto_cancel_enabled - - orders + - rfq properties: - is_auto_cancel_enabled: - type: boolean - description: Whether auto-cancel is enabled for this order group - contracts_limit_fp: - $ref: '#/components/schemas/FixedPointCount' - description: String representation of the current maximum contracts allowed over a rolling 15-second window. - x-go-type-skip-optional-pointer: true - orders: - type: array - items: - type: string - description: List of order IDs that belong to this order group - x-go-type-skip-optional-pointer: true - exchange_index: - allOf: - - $ref: '#/components/schemas/ExchangeIndex' - x-go-type-skip-optional-pointer: true - x-omitempty: false + rfq: + $ref: '#/components/schemas/RFQ' + description: The details of the requested RFQ - CreateOrderGroupRequest: + CreateRFQRequest: type: object + required: + - market_ticker + - rest_remainder properties: - subaccount: - type: integer - minimum: 0 - description: Optional subaccount number to use for this order group (0 for primary, 1-63 for subaccounts). Subaccount-restricted API keys must omit this field or pass their locked subaccount. - contracts_limit: + market_ticker: + type: string + description: The ticker of the market for which to create an RFQ + contracts: type: integer - format: int64 - minimum: 1 - description: Specifies the maximum number of contracts that can be matched within this group over a rolling 15-second window. Whole contracts only. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + description: Whole-contract count for the RFQ. Use contracts_fp for partial contract values; if both are provided, they must match. x-go-type-skip-optional-pointer: true - x-oapi-codegen-extra-tags: - validate: omitempty,gte=1 - contracts_limit_fp: + contracts_fp: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: String representation of the maximum number of contracts that can be matched within this group over a rolling 15-second window. Provide contracts_limit or contracts_limit_fp; if both provided they must match. - exchange_index: - allOf: - - $ref: '#/components/schemas/ExchangeIndex' - default: 0 - description: Identifier for an exchange shard. Defaults to 0. - x-go-type-skip-optional-pointer: true - - UpdateOrderGroupLimitRequest: - type: object - properties: - contracts_limit: + description: Fixed-point number of contracts for the RFQ. Supports partial contracts in 0.01-contract increments; if contracts is also provided, both values must match. + target_cost_centi_cents: type: integer format: int64 - minimum: 1 - description: New maximum number of contracts that can be matched within this group over a rolling 15-second window. Whole contracts only. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + description: 'DEPRECATED: The target cost for the RFQ in centi-cents. Use target_cost_dollars instead.' + deprecated: true x-go-type-skip-optional-pointer: true - x-oapi-codegen-extra-tags: - validate: omitempty,gte=1 - contracts_limit_fp: - $ref: '#/components/schemas/FixedPointCount' - nullable: true - description: String representation of the new maximum number of contracts that can be matched within this group over a rolling 15-second window. Provide contracts_limit or contracts_limit_fp; if both provided they must match. - - CreateOrderGroupResponse: - type: object - required: - - order_group_id - - subaccount - properties: - order_group_id: + target_cost_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: The target cost for the RFQ in dollars + x-go-type-skip-optional-pointer: true + target_cost_excludes_fees: + type: boolean + description: >- + Sizes quotes against the target cost as principal only (contracts = + target cost / price), with your taker fees charged on top of the + target cost. By default (false) the target cost caps principal plus + 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: Replace the RFQ creator's public communications ID with a shared empty-input SHA-256 hash for other users. Applies to RFQ responses and broadcasts, and to quotes before successful execution. Successful execution reveals the normal ID in subsequent RFQ responses and in the executed quote. Accepting or confirming a quote does not reveal the ID. The creator always sees their normal ID. See the RFQ guide for the placeholder and visibility rules. + default: false + x-go-type-skip-optional-pointer: true + rest_remainder: + type: boolean + description: Whether to rest the remainder of the RFQ after execution + replace_existing: + type: boolean + description: Whether to delete existing RFQs as part of this RFQ's creation + default: false + x-go-type-skip-optional-pointer: true + subtrader_id: type: string - description: The unique identifier for the created order group + description: The subtrader to create the RFQ for (FCM members only) + x-go-type-skip-optional-pointer: true subaccount: type: integer - minimum: 0 - description: Subaccount number that owns the created order group (0 for primary, 1-63 for subaccounts). - x-go-type-skip-optional-pointer: true - exchange_index: - allOf: - - $ref: '#/components/schemas/ExchangeIndex' - x-go-type-skip-optional-pointer: true - x-omitempty: false + description: The subaccount number to create the RFQ for (direct members only; 0 for primary, 1-63 for subaccounts) - GetCommunicationsIDResponse: + CreateRFQResponse: type: object required: - - communications_id + - id properties: - communications_id: + id: type: string - description: A public communications ID which is used to identify the user + description: UUID of the newly created RFQ. Pass it unchanged in subsequent requests. - BlockTradeProposal: + Quote: type: object required: - id - - proposer_user_id - - buyer_user_id - - seller_user_id + - rfq_id + - creator_id + - rfq_creator_id - market_ticker - - price_centi_cents - - centicount - - maker_side - - expiration_ts - - status + - contracts_fp + - yes_bid_dollars + - no_bid_dollars - created_ts - updated_ts - - buyer_accepted - - seller_accepted + - status properties: id: type: string - description: Unique identifier for the block trade proposal - proposer_user_id: + description: UUID of the quote. Preserve the exact returned string. + rfq_id: type: string - description: User ID of the proposal creator - buyer_user_id: + description: UUID of the RFQ this quote is responding to. + creator_id: type: string - description: User ID of the buyer. Empty when the authenticated user is not the buyer. - buyer_subtrader_id: + description: Public communications ID of the quote creator + rfq_creator_id: type: string - description: Subtrader ID of the buyer. Empty when the authenticated user is not the buyer. + description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. After this quote executes successfully, its counterparty sees the normal public communications ID. x-go-type-skip-optional-pointer: true - seller_user_id: + market_ticker: type: string - description: User ID of the seller. Empty when the authenticated user is not the seller. - seller_subtrader_id: + description: The ticker of the market this quote is for + contracts_fp: + $ref: '#/components/schemas/FixedPointCount' + description: String representation of the number of contracts in the quote + yes_bid_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: Bid price for YES contracts, in dollars + no_bid_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: Bid price for NO contracts, in dollars + created_ts: type: string - description: Subtrader ID of the seller. Empty when the authenticated user is not the seller. - x-go-type-skip-optional-pointer: true - market_ticker: + format: date-time + description: Timestamp when the quote was created + updated_ts: type: string - description: The ticker of the market for this block trade - price_centi_cents: - type: integer - format: int64 - description: Price in centi-cents - centicount: - type: integer - format: int64 - description: Number of contracts in centicounts - maker_side: + format: date-time + description: Timestamp when the quote was last updated + status: type: string - description: The maker side of the trade + description: Current status of the quote + enum: [open, accepted, confirmed, executed, cancelled] + accepted_side: + type: string + description: The side that was accepted (yes or no) enum: ['yes', 'no'] - expiration_ts: + accepted_ts: type: string format: date-time - description: Expiration time of the proposal - status: + description: Timestamp when the quote was accepted + confirmed_ts: type: string - description: Current status of the proposal - created_ts: + format: date-time + description: Timestamp when the quote was confirmed + executed_ts: type: string format: date-time - description: Timestamp when the proposal was created - updated_ts: + description: Timestamp when the quote was executed + cancelled_ts: type: string format: date-time - description: Timestamp when the proposal was last updated - buyer_accepted: + description: Timestamp when the quote was cancelled + rest_remainder: type: boolean - description: Whether the buyer has accepted the proposal - seller_accepted: + description: Whether to rest the remainder of the quote after execution + post_only: type: boolean - description: Whether the seller has accepted the proposal - buyer_accepted_ts: + description: Whether the quote creator's order is post-only (visible when the caller is the quote creator) + cancellation_reason: type: string - format: date-time - description: Timestamp when the buyer accepted - seller_accepted_ts: + description: Reason for quote cancellation if cancelled + x-go-type-skip-optional-pointer: true + creator_user_id: type: string - format: date-time - description: Timestamp when the seller accepted - executed_ts: + description: User ID of the quote creator (private field) + x-go-type-skip-optional-pointer: true + rfq_creator_user_id: type: string - format: date-time - description: Timestamp when the proposal was executed - buyer_order_id: + description: User ID of the RFQ creator (private field) + x-go-type-skip-optional-pointer: true + rfq_target_cost_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: Total value requested in the RFQ in dollars + target_cost_excludes_fees: + type: boolean + description: >- + True when the RFQ's target cost is principal-only and the + contracts-offered sizes were computed without reserving taker fees + (fees are charged on top of the target cost). + x-go-type-skip-optional-pointer: true + rfq_creator_order_id: type: string - description: Order ID for the buyer after the proposal is executed + description: Order ID for the RFQ creator (private field) x-go-type-skip-optional-pointer: true - seller_order_id: + creator_order_id: type: string - description: Order ID for the seller after the proposal is executed + description: Order ID for the quote creator (private field) x-go-type-skip-optional-pointer: true + creator_subaccount: + type: integer + description: Subaccount number of the quote creator (visible when the caller is the quote creator) + rfq_creator_subaccount: + type: integer + description: Subaccount number of the RFQ creator (visible when the caller is the RFQ creator) + yes_contracts_fp: + $ref: '#/components/schemas/FixedPointCount' + description: Number of YES contracts offered in the quote (fixed-point) + no_contracts_fp: + $ref: '#/components/schemas/FixedPointCount' + description: Number of NO contracts offered in the quote (fixed-point) - GetBlockTradeProposalsResponse: + GetQuotesResponse: type: object required: - - block_trade_proposals + - quotes properties: - block_trade_proposals: + quotes: type: array items: - $ref: '#/components/schemas/BlockTradeProposal' - description: List of block trade proposals + $ref: '#/components/schemas/Quote' + description: List of quotes matching the query criteria cursor: type: string description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - ProposeBlockTradeRequest: + GetQuoteResponse: type: object required: - - buyer_user_id - - seller_user_id - - market_ticker - - price_centi_cents - - centicount - - maker_side - - expiration_ts + - quote properties: - buyer_user_id: - type: string - description: User ID of the buyer - x-oapi-codegen-extra-tags: - validate: required - buyer_subtrader_id: - type: string - description: Subtrader ID of the buyer. Provide either this or buyer_subaccount, not both. - x-go-type-skip-optional-pointer: true - buyer_subaccount: - type: integer - minimum: 0 - maximum: 63 - description: User-managed subaccount number of the buyer (0 for primary, 1-63 for numbered subaccounts). Provide either this or buyer_subtrader_id, not both. - seller_user_id: + quote: + $ref: '#/components/schemas/Quote' + description: The details of the requested quote + + CreateQuoteRequest: + type: object + required: + - rfq_id + - yes_bid + - no_bid + - rest_remainder + properties: + rfq_id: type: string - description: User ID of the seller - x-oapi-codegen-extra-tags: - validate: required - seller_subtrader_id: + description: The UUID of the RFQ to quote on. Pass the RFQ ID unchanged; malformed IDs return HTTP 400. + yes_bid: type: string - description: Subtrader ID of the seller. Provide either this or seller_subaccount, not both. - x-go-type-skip-optional-pointer: true - seller_subaccount: - type: integer - minimum: 0 - maximum: 63 - description: User-managed subaccount number of the seller (0 for primary, 1-63 for numbered subaccounts). Provide either this or seller_subtrader_id, not both. - market_ticker: + $ref: '#/components/schemas/FixedPointDollars' + description: The bid price for YES contracts, in dollars + no_bid: type: string - description: The ticker of the market for this block trade - x-oapi-codegen-extra-tags: - validate: required - price_centi_cents: - type: integer - format: int64 - minimum: 1 - description: Price in centi-cents - x-oapi-codegen-extra-tags: - validate: required,gt=0 - centicount: + $ref: '#/components/schemas/FixedPointDollars' + description: The bid price for NO contracts, in dollars + rest_remainder: + type: boolean + description: Whether to rest the remainder of the quote after execution + post_only: + type: boolean + description: If true, the quote creator's resting order will be cancelled rather than crossed if it would take liquidity. Defaults to false. + subaccount: type: integer - format: int64 - minimum: 1 - description: Number of contracts in centicounts - x-oapi-codegen-extra-tags: - validate: required,gt=0 - maker_side: - type: string - description: The maker side of the trade - enum: ['yes', 'no'] - x-oapi-codegen-extra-tags: - validate: required,oneof=yes no - expiration_ts: - type: string - format: date-time - description: Expiration time of the proposal - x-oapi-codegen-extra-tags: - validate: required + description: Optional subaccount number to place the quote under (0 for primary, 1-63 for subaccounts) - ProposeBlockTradeResponse: + CreateQuoteResponse: type: object required: - - block_trade_proposal_id + - id properties: - block_trade_proposal_id: + id: type: string - description: The ID of the newly created block trade proposal + description: UUID of the newly created quote. Pass it unchanged in subsequent requests. - AcceptBlockTradeProposalRequest: + AcceptQuoteRequest: type: object + required: + - accepted_side properties: - subtrader_id: + accepted_side: type: string - description: Subtrader ID to accept as. Provide either this or subaccount, not both. - x-go-type-skip-optional-pointer: true - subaccount: - type: integer - minimum: 0 - maximum: 63 - description: User-managed subaccount number to accept as (0 for primary, 1-63 for numbered subaccounts). Provide either this or subtrader_id, not both. + description: The side of the quote to accept (yes or no) + enum: ['yes', 'no'] - RFQ: + RFQBatchStatus: + type: string + description: Status of an RFQ batch. An open batch takes batch quotes; an accepted batch is waiting for its winning makers to confirm or is executing; a closed batch is finished. + enum: [open, accepted, closed] + + RFQBatch: type: object required: - id - creator_id - - contracts_fp - - market_ticker - status - created_ts + - rfqs properties: id: type: string - description: UUID of the RFQ. Preserve the exact returned string. + description: UUID of the RFQ batch. Preserve the exact returned string. creator_id: type: string - description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. After successful quote execution, subsequent RFQ responses show the normal public communications ID to all viewers. - market_ticker: - type: string - description: The ticker of the market this RFQ is for - contracts_fp: - $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts requested in the RFQ - target_cost_dollars: - $ref: '#/components/schemas/FixedPointDollars' - description: Total value of the RFQ in dollars - target_cost_excludes_fees: - type: boolean - description: >- - True when the target cost is principal-only and quote sizes are - computed without reserving taker fees (fees are charged on top). - x-go-type-skip-optional-pointer: true + description: Public communications ID of the batch creator, obscured for other users like RFQ.creator_id when obscure_creator_id was set. It is revealed once a child RFQ executes. status: - type: string - description: Current status of the RFQ (open, closed) - enum: [open, closed] + $ref: '#/components/schemas/RFQBatchStatus' created_ts: type: string format: date-time - description: Timestamp when the RFQ was created - mve_collection_ticker: + description: Timestamp when the RFQ batch was created + accepted_ts: type: string - description: Ticker of the MVE collection this market belongs to - x-go-type-skip-optional-pointer: true - mve_selected_legs: + format: date-time + description: Timestamp when a batch quote on the batch was accepted + closed_ts: + type: string + format: date-time + description: Timestamp when the RFQ batch closed + rfqs: type: array - x-omitempty: true items: - $ref: '#/components/schemas/MveSelectedLeg' - description: Selected legs for the MVE collection + $ref: '#/components/schemas/RFQ' + description: The child RFQs, in the order they were requested + updated_ts: + type: string + format: date-time + 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) x-go-type-skip-optional-pointer: true - rest_remainder: - type: boolean - description: Whether to rest the remainder of the RFQ after execution - cancellation_reason: + client_batch_id: type: string - description: Reason for RFQ cancellation if cancelled + description: The client_batch_id given at creation (visible to the batch creator) x-go-type-skip-optional-pointer: true creator_user_id: type: string - description: User ID of the RFQ creator (private field) + description: User ID of the batch creator (private field) x-go-type-skip-optional-pointer: true creator_subaccount: type: integer - description: Subaccount number of the RFQ creator (visible when the caller is the RFQ creator) - cancelled_ts: - type: string - format: date-time - description: Timestamp when the RFQ was cancelled - updated_ts: - type: string - format: date-time - description: Timestamp when the RFQ was last updated + description: Subaccount number of the batch creator (visible to the batch creator) - GetRFQsResponse: + GetRFQBatchResponse: type: object required: - - rfqs + - rfq_batch properties: - rfqs: + rfq_batch: + $ref: '#/components/schemas/RFQBatch' + + GetRFQBatchesResponse: + type: object + required: + - rfq_batches + properties: + rfq_batches: type: array items: - $ref: '#/components/schemas/RFQ' - description: List of RFQs matching the query criteria + $ref: '#/components/schemas/RFQBatch' + description: RFQ batches matching the query criteria cursor: type: string description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - GetRFQResponse: - type: object - required: - - rfq + CreateRFQBatchChild: + type: object + description: One combo to request quotes on. Set exactly one of market_ticker and selected_markets. properties: - rfq: - $ref: '#/components/schemas/RFQ' - description: The details of the requested RFQ + market_ticker: + type: string + description: Ticker of an existing combo market + x-go-type-skip-optional-pointer: true + selected_markets: + type: array + maxItems: 6 + items: + $ref: '#/components/schemas/TickerPair' + description: Legs of the combo, at most 6, looked up or created in the request's mve_collection_ticker as POST /multivariate_event_collections/{collection_ticker} does. + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: omitempty,max=6,dive - CreateRFQRequest: + CreateRFQBatchRequest: type: object required: - - market_ticker - - rest_remainder + - children properties: - market_ticker: + children: + type: array + minItems: 2 + maxItems: 57 + items: + $ref: '#/components/schemas/CreateRFQBatchChild' + description: The combos to request quotes on, one child RFQ each. Children must be distinct combos. + x-oapi-codegen-extra-tags: + validate: required,min=2,max=57,dive + mve_collection_ticker: type: string - description: The ticker of the market for which to create an RFQ - contracts: - type: integer - description: Whole-contract count for the RFQ. Use contracts_fp for partial contract values; if both are provided, they must match. + description: Collection in which children given by selected_markets are looked up or created. Required when any child uses selected_markets. x-go-type-skip-optional-pointer: true contracts_fp: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: Fixed-point number of contracts for the RFQ. Supports partial contracts in 0.01-contract increments; if contracts is also provided, both values must match. - target_cost_centi_cents: - type: integer - format: int64 - description: 'DEPRECATED: The target cost for the RFQ in centi-cents. Use target_cost_dollars instead.' - deprecated: true - x-go-type-skip-optional-pointer: true + description: Number of contracts requested on every child. Provide exactly one of contracts_fp and target_cost_dollars. target_cost_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: The target cost for the RFQ in dollars + description: Target cost of every child, in dollars. Provide exactly one of contracts_fp and target_cost_dollars. x-go-type-skip-optional-pointer: true target_cost_excludes_fees: type: boolean - description: >- - Sizes quotes against the target cost as principal only (contracts = - target cost / price), with your taker fees charged on top of the - target cost. By default (false) the target cost caps principal plus - Kalshi fees, and quote sizes are reduced to make room for the fees. - Only valid together with a target cost. + description: Same as on Create RFQ, applied to every child. Only valid together with target_cost_dollars. x-go-type-skip-optional-pointer: true - obscure_creator_id: + rest_remainder: type: boolean - description: Replace the RFQ creator's public communications ID with a shared empty-input SHA-256 hash for other users. Applies to RFQ responses and broadcasts, and to quotes before successful execution. Successful execution reveals the normal ID in subsequent RFQ responses and in the executed quote. Accepting or confirming a quote does not reveal the ID. The creator always sees their normal ID. See the RFQ guide for the placeholder and visibility rules. + description: Whether to rest the remainder of each child RFQ after execution default: false x-go-type-skip-optional-pointer: true - rest_remainder: - type: boolean - description: Whether to rest the remainder of the RFQ after execution replace_existing: type: boolean - description: Whether to delete existing RFQs as part of this RFQ's creation + description: Close your oldest open RFQ batches as needed so this one can be created. Single RFQs are never replaced. + default: false + x-go-type-skip-optional-pointer: true + obscure_creator_id: + type: boolean + description: Same as on Create RFQ, applied to the batch and every child. default: false x-go-type-skip-optional-pointer: true + client_batch_id: + type: string + maxLength: 64 + x-oapi-codegen-extra-tags: + validate: "omitempty,max=64" + description: Optional key for this batch, unique among your RFQ batches, including those closed within the last seven days. Reusing one returns HTTP 409. + x-go-type-skip-optional-pointer: true subtrader_id: type: string - description: The subtrader to create the RFQ for (FCM members only) + description: The subtrader to create the RFQ batch for (FCM members only) x-go-type-skip-optional-pointer: true subaccount: type: integer - description: The subaccount number to create the RFQ for (direct members only; 0 for primary, 1-63 for subaccounts) + description: The subaccount number to create the RFQ batch for (direct members only; 0 for primary, 1-63 for subaccounts) - CreateRFQResponse: + CreateRFQBatchResponse: type: object required: - - id + - rfq_batch properties: - id: - type: string - description: UUID of the newly created RFQ. Pass it unchanged in subsequent requests. + rfq_batch: + $ref: '#/components/schemas/RFQBatch' - Quote: + RFQBatchQuoteStatus: + type: string + description: Status of a batch quote. A partially_executed batch quote executed some children and failed others. + enum: [open, accepted, confirmed, executed, partially_executed, cancelled] + + RFQBatchQuote: type: object + description: One maker's quote on an RFQ batch. Once the batch is accepted, quotes lists only the children this maker won. required: - id - - rfq_id + - rfq_batch_id - creator_id - rfq_creator_id - - market_ticker - - contracts_fp - - yes_bid_dollars - - no_bid_dollars + - status - created_ts - updated_ts - - status + - quotes properties: id: type: string - description: UUID of the quote. Preserve the exact returned string. - rfq_id: + description: UUID of the batch quote. Preserve the exact returned string. + rfq_batch_id: type: string - description: UUID of the RFQ this quote is responding to. + description: UUID of the RFQ batch this batch quote prices creator_id: type: string - description: Public communications ID of the quote creator + description: Public communications ID of the batch quote creator rfq_creator_id: type: string - description: Public communications ID of the RFQ creator (pseudonymous). When obscure_creator_id is enabled, other users receive the shared placeholder "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" (SHA-256 of empty input). The creator sees their normal ID. After this quote executes successfully, its counterparty sees the normal public communications ID. - x-go-type-skip-optional-pointer: true - market_ticker: - type: string - description: The ticker of the market this quote is for - contracts_fp: - $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts in the quote - yes_bid_dollars: - $ref: '#/components/schemas/FixedPointDollars' - description: Bid price for YES contracts, in dollars - no_bid_dollars: - $ref: '#/components/schemas/FixedPointDollars' - description: Bid price for NO contracts, in dollars + description: Public communications ID of the RFQ batch creator, obscured like Quote.rfq_creator_id + status: + $ref: '#/components/schemas/RFQBatchQuoteStatus' created_ts: type: string format: date-time - description: Timestamp when the quote was created + description: Timestamp when the batch quote was created updated_ts: type: string format: date-time - description: Timestamp when the quote was last updated - status: - type: string - description: Current status of the quote - enum: [open, accepted, confirmed, executed, cancelled] + description: Timestamp when the batch quote was last updated accepted_side: type: string - description: The side that was accepted (yes or no) + description: The side that was accepted on this maker's children (yes or no) enum: ['yes', 'no'] + x-enum-varnames: ['RFQBatchQuoteAcceptedSideYes', 'RFQBatchQuoteAcceptedSideNo'] accepted_ts: type: string format: date-time - description: Timestamp when the quote was accepted + description: Timestamp when the batch quote was accepted confirmed_ts: type: string format: date-time - description: Timestamp when the quote was confirmed - executed_ts: - type: string - format: date-time - description: Timestamp when the quote was executed - cancelled_ts: - type: string - format: date-time - description: Timestamp when the quote was cancelled + description: Timestamp when the batch quote was confirmed + quotes: + type: array + items: + $ref: '#/components/schemas/Quote' + description: One quote per child RFQ, or once accepted one per child this maker won; match them to children by rfq_id. Each accepted child quote executes as its own trade. rest_remainder: type: boolean - description: Whether to rest the remainder of the quote after execution + description: Whether to rest the remainder of each child quote after execution (visible to the batch quote creator) post_only: type: boolean - description: Whether the quote creator's order is post-only (visible when the caller is the quote creator) - cancellation_reason: - type: string - description: Reason for quote cancellation if cancelled - x-go-type-skip-optional-pointer: true + description: Whether the batch quote creator's orders are post-only (visible to the batch quote creator) creator_user_id: type: string - description: User ID of the quote creator (private field) + description: User ID of the batch quote creator (private field) x-go-type-skip-optional-pointer: true + creator_subaccount: + type: integer + description: Subaccount number of the batch quote creator (visible to the batch quote creator) rfq_creator_user_id: type: string - description: User ID of the RFQ creator (private field) - x-go-type-skip-optional-pointer: true - rfq_target_cost_dollars: - $ref: '#/components/schemas/FixedPointDollars' - description: Total value requested in the RFQ in dollars - target_cost_excludes_fees: - type: boolean - description: >- - True when the RFQ's target cost is principal-only and the - contracts-offered sizes were computed without reserving taker fees - (fees are charged on top of the target cost). - x-go-type-skip-optional-pointer: true - rfq_creator_order_id: - type: string - description: Order ID for the RFQ creator (private field) - x-go-type-skip-optional-pointer: true - creator_order_id: - type: string - description: Order ID for the quote creator (private field) + description: User ID of the RFQ batch creator (private field) x-go-type-skip-optional-pointer: true - creator_subaccount: - type: integer - description: Subaccount number of the quote creator (visible when the caller is the quote creator) rfq_creator_subaccount: type: integer - description: Subaccount number of the RFQ creator (visible when the caller is the RFQ creator) - yes_contracts_fp: - $ref: '#/components/schemas/FixedPointCount' - description: Number of YES contracts offered in the quote (fixed-point) - no_contracts_fp: - $ref: '#/components/schemas/FixedPointCount' - description: Number of NO contracts offered in the quote (fixed-point) + description: Subaccount number of the RFQ batch creator (visible to the RFQ batch creator) - GetQuotesResponse: + GetRFQBatchQuotesResponse: type: object required: - - quotes + - batch_quotes properties: - quotes: + batch_quotes: type: array items: - $ref: '#/components/schemas/Quote' - description: List of quotes matching the query criteria - cursor: - type: string - description: Cursor for pagination to get the next page of results - x-go-type-skip-optional-pointer: true - - GetQuoteResponse: - type: object - required: - - quote - properties: - quote: - $ref: '#/components/schemas/Quote' - description: The details of the requested quote + $ref: '#/components/schemas/RFQBatchQuote' + description: Batch quotes on the RFQ batch visible to you - CreateQuoteRequest: + RFQBatchQuoteEntry: type: object required: - rfq_id - yes_bid - no_bid - - rest_remainder properties: rfq_id: type: string - description: The UUID of the RFQ to quote on. Pass the RFQ ID unchanged; malformed IDs return HTTP 400. + description: UUID of the child RFQ to price. Pass it unchanged; malformed IDs return HTTP 400. yes_bid: - type: string $ref: '#/components/schemas/FixedPointDollars' - description: The bid price for YES contracts, in dollars + description: The bid price for YES contracts on this child, in dollars no_bid: - type: string $ref: '#/components/schemas/FixedPointDollars' - description: The bid price for NO contracts, in dollars + description: The bid price for NO contracts on this child, in dollars + + CreateRFQBatchQuoteRequest: + type: object + required: + - quotes + - rest_remainder + properties: + quotes: + type: array + minItems: 2 + maxItems: 57 + items: + $ref: '#/components/schemas/RFQBatchQuoteEntry' + description: Prices for every child RFQ of the batch, one entry per child. + x-oapi-codegen-extra-tags: + validate: required,min=2,max=57,dive rest_remainder: type: boolean - description: Whether to rest the remainder of the quote after execution + description: Whether to rest the remainder of each child quote after execution post_only: type: boolean - description: If true, the quote creator's resting order will be cancelled rather than crossed if it would take liquidity. Defaults to false. + description: If true, your resting orders are cancelled rather than crossed if they would take liquidity. Defaults to false. subaccount: type: integer - description: Optional subaccount number to place the quote under (0 for primary, 1-63 for subaccounts) + description: Optional subaccount number to place the batch quote under (0 for primary, 1-63 for subaccounts) - CreateQuoteResponse: + CreateRFQBatchQuoteResponse: type: object required: - id properties: id: type: string - description: UUID of the newly created quote. Pass it unchanged in subsequent requests. + description: UUID of the newly created batch quote. Pass it unchanged in subsequent requests. - AcceptQuoteRequest: + RFQBatchBestQuote: + type: object + required: + - rfq_id + - market_ticker + properties: + rfq_id: + type: string + description: UUID of the child RFQ + market_ticker: + type: string + description: The child's combo market + yes_bid_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: Best YES bid across makers, in dollars; absent when no maker bids YES + no_bid_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: Best NO bid across makers, in dollars; absent when no maker bids NO + yes_contracts_offered_fp: + $ref: '#/components/schemas/FixedPointCount' + description: Contracts the best YES bid offers + no_contracts_offered_fp: + $ref: '#/components/schemas/FixedPointCount' + description: Contracts the best NO bid offers + + RFQBatchBestQuotes: + type: object + required: + - rfq_batch_id + - status + - version + - yes_complete + - no_complete + - rfqs + properties: + rfq_batch_id: + type: string + description: UUID of the RFQ batch + status: + $ref: '#/components/schemas/RFQBatchStatus' + version: + type: integer + format: int64 + description: Increases whenever a best bid changes + updated_ts: + type: string + format: date-time + description: When a best bid last changed + yes_complete: + type: boolean + description: Whether every child has a YES bid, so the batch can be accepted on the yes side + no_complete: + type: boolean + description: Whether every child has a NO bid, so the batch can be accepted on the no side + rfqs: + type: array + items: + $ref: '#/components/schemas/RFQBatchBestQuote' + description: One entry per child RFQ, in batch order + + GetRFQBatchBestQuotesResponse: + type: object + required: + - best_quotes + properties: + best_quotes: + $ref: '#/components/schemas/RFQBatchBestQuotes' + + RFQBatchAcceptLimit: + type: object + required: + - rfq_id + - bid_dollars + properties: + rfq_id: + type: string + description: UUID of the child RFQ. Pass it unchanged; malformed IDs return HTTP 400. + bid_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: The lowest bid you take on this child on the accepted side, normally the best bid you were shown + + AcceptRFQBatchRequest: type: object required: - accepted_side + - min_bids properties: accepted_side: type: string - description: The side of the quote to accept (yes or no) + description: The side to accept on every child (yes or no) + enum: ['yes', 'no'] + x-enum-varnames: ['AcceptRFQBatchRequestAcceptedSideYes', 'AcceptRFQBatchRequestAcceptedSideNo'] + min_bids: + type: array + minItems: 2 + maxItems: 57 + items: + $ref: '#/components/schemas/RFQBatchAcceptLimit' + description: Price protection, exactly one entry per child. A child whose best bid on the accepted side is now below its entry fails the accept. + x-oapi-codegen-extra-tags: + validate: required,min=2,max=57,dive + client_order_id: + type: string + description: Optional idempotency key. Retrying an accept with the same client_order_id returns the result of the accept that succeeded. + x-go-type-skip-optional-pointer: true + + AcceptRFQBatchResponse: + type: object + required: + - accepted_side + - quotes + properties: + accepted_side: + type: string + description: The side accepted on every child (yes or no) enum: ['yes', 'no'] + x-enum-varnames: ['AcceptRFQBatchResponseAcceptedSideYes', 'AcceptRFQBatchResponseAcceptedSideNo'] + quotes: + type: array + items: + $ref: '#/components/schemas/Quote' + description: The quote each child was accepted from, in batch order. Each winning maker then confirms its children. # Order schemas GetOrderResponse: diff --git a/specs/perps_scm_openapi.yaml b/specs/perps_scm_openapi.yaml index 7530222b..487bd7b1 100644 --- a/specs/perps_scm_openapi.yaml +++ b/specs/perps_scm_openapi.yaml @@ -978,11 +978,11 @@ components: reportable_threshold: { type: integer, format: int64, description: 'CFTC reporting level for this market, in contracts (17 CFR 15.03(b)). A position of at least this size in either direction is reportable.' } EstimatePortfolioMaintenanceMarginPosition: type: object - required: [market_ticker, quantity, price] + required: [market_ticker, quantity] properties: market_ticker: { type: string, description: Market to include in the hypothetical portfolio. Must belong to the request's asset class. } quantity: { type: integer, format: int64, description: "Signed whole contract count; positive long, negative short. Must be non-zero." } - price: { type: string, description: 'Price of one contract in USD, to at most four decimal places (e.g. "6.8000"). Must be positive.' } + price: { type: string, description: 'Price of one contract in USD, to at most four decimal places (e.g. "6.8000"). Must be positive. Omit to use the market''s mark price from its latest settlement (before the end of `date`, if set); returns 400 if there is none.' } EstimatePortfolioMaintenanceMarginRequest: type: object diff --git a/tests/_contract_support.py b/tests/_contract_support.py index 02170dc0..7eb9f85c 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -674,6 +674,65 @@ class Exclusion: path_template="/communications/block-trade-proposals/{block_trade_proposal_id}/accept", request_body_schema="#/components/schemas/AcceptBlockTradeProposalRequest", ), + # RFQ batches + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.list", + http_method="GET", + path_template="/communications/rfq_batches", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.list_all", + http_method="GET", + path_template="/communications/rfq_batches", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.create", + http_method="POST", + path_template="/communications/rfq_batches", + request_body_schema="#/components/schemas/CreateRFQBatchRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.get", + http_method="GET", + path_template="/communications/rfq_batches/{rfq_batch_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.delete", + http_method="DELETE", + path_template="/communications/rfq_batches/{rfq_batch_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.best_quotes", + http_method="GET", + path_template="/communications/rfq_batches/{rfq_batch_id}/best_quotes", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchesResource.accept", + http_method="PUT", + path_template="/communications/rfq_batches/{rfq_batch_id}/accept", + request_body_schema="#/components/schemas/AcceptRFQBatchRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchQuotesResource.list", + http_method="GET", + path_template="/communications/rfq_batches/{rfq_batch_id}/quotes", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchQuotesResource.create", + http_method="POST", + path_template="/communications/rfq_batches/{rfq_batch_id}/quotes", + request_body_schema="#/components/schemas/CreateRFQBatchQuoteRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchQuotesResource.delete", + http_method="DELETE", + path_template="/communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQBatchQuotesResource.confirm", + http_method="PUT", + path_template="/communications/rfq_batches/{rfq_batch_id}/quotes/{batch_quote_id}/confirm", + ), # ── subaccounts ───────────────────────────────────────────────────────── MethodEndpointEntry( sdk_method="kalshi.resources.subaccounts.SubaccountsResource.create", @@ -1146,6 +1205,13 @@ class Exclusion: reason="paginator-handled; not a caller-facing kwarg on list_all", kind="paginator_handled", ), + ( + "kalshi.resources.communications.RFQBatchesResource.list_all", + "cursor", + ): Exclusion( + reason="paginator-handled; not a caller-facing kwarg on list_all", + kind="paginator_handled", + ), ("kalshi.resources.markets.MarketsResource.list_all_trades", "cursor"): Exclusion( reason="paginator-handled; not a caller-facing kwarg on list_all", kind="paginator_handled", @@ -1350,6 +1416,7 @@ class Exclusion: "kalshi.resources.communications.RFQsResource.list_all", "kalshi.resources.communications.QuotesResource.list_all", "kalshi.resources.communications.BlockTradeProposalsResource.list_all", + "kalshi.resources.communications.RFQBatchesResource.list_all", "kalshi.resources.milestones.MilestonesResource.list_all", "kalshi.resources.events.EventsResource.list_all", "kalshi.resources.events.EventsResource.list_all_multivariate", @@ -1383,6 +1450,7 @@ class Exclusion: "kalshi.resources.communications.AsyncRFQsResource.list_all", "kalshi.resources.communications.AsyncQuotesResource.list_all", "kalshi.resources.communications.AsyncBlockTradeProposalsResource.list_all", + "kalshi.resources.communications.AsyncRFQBatchesResource.list_all", "kalshi.resources.milestones.AsyncMilestonesResource.list_all", "kalshi.resources.events.AsyncEventsResource.list_all", "kalshi.resources.events.AsyncEventsResource.list_all_multivariate", diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 0100ee0a..f63e4b1c 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -229,7 +229,7 @@ async def fake_sleep(d: float) -> None: side_effect=[ httpx.Response( 429, - headers={"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"}, + headers={"Retry-After": "Tue, 01 Jan 2030 00:00:00 GMT"}, json={"message": "rl"}, ), httpx.Response(200, json={"markets": []}), diff --git a/tests/test_communications.py b/tests/test_communications.py index b02d2484..0193d284 100644 --- a/tests/test_communications.py +++ b/tests/test_communications.py @@ -25,17 +25,29 @@ RFQ, AcceptBlockTradeProposalRequest, AcceptQuoteRequest, + AcceptRFQBatchRequest, BlockTradeProposal, CreateQuoteRequest, CreateQuoteResponse, + CreateRFQBatchChild, + CreateRFQBatchQuoteRequest, + CreateRFQBatchRequest, CreateRFQRequest, CreateRFQResponse, GetBlockTradeProposalsResponse, GetCommunicationsIDResponse, GetQuoteResponse, + GetRFQBatchBestQuotesResponse, + GetRFQBatchQuotesResponse, + GetRFQBatchResponse, GetRFQResponse, ProposeBlockTradeRequest, Quote, + RFQBatch, + RFQBatchAcceptLimit, + RFQBatchBestQuote, + RFQBatchQuote, + RFQBatchQuoteEntry, ) from kalshi.resources.communications import ( AsyncCommunicationsResource, @@ -43,6 +55,8 @@ AsyncRFQsResource, CommunicationsResource, QuotesResource, + RFQBatchesResource, + RFQBatchQuotesResource, RFQsResource, ) @@ -1773,3 +1787,736 @@ def test_create_requires_auth(self, unauth_comms: CommunicationsResource) -> Non def test_accept_requires_auth(self, unauth_comms: CommunicationsResource) -> None: with pytest.raises(AuthRequiredError): unauth_comms.block_trade_proposals.accept("btp-1") + + +# ── RFQ batches ───────────────────────────────────────────────────────────── + + +_MINIMAL_RFQ_BATCH = { + "id": "batch-1", + "creator_id": "comm-42", + "status": "open", + "created_ts": "2026-04-18T12:00:00Z", + "rfqs": [_MINIMAL_RFQ, {**_MINIMAL_RFQ, "id": "rfq-2", "market_ticker": "MKT-2"}], +} + +_MINIMAL_BATCH_QUOTE = { + "id": "bq-1", + "rfq_batch_id": "batch-1", + "creator_id": "comm-99", + "rfq_creator_id": "comm-42", + "status": "open", + "created_ts": "2026-04-18T12:01:00Z", + "updated_ts": "2026-04-18T12:01:00Z", + "quotes": [_MINIMAL_QUOTE], +} + +_MINIMAL_BEST_QUOTES = { + "rfq_batch_id": "batch-1", + "status": "open", + "version": 3, + "yes_complete": True, + "no_complete": False, + "rfqs": [ + { + "rfq_id": "rfq-1", + "market_ticker": "MKT-1", + "yes_bid_dollars": "0.5600", + "no_bid_dollars": "0.4400", + "yes_contracts_offered_fp": "10.00", + "no_contracts_offered_fp": "10.00", + }, + {"rfq_id": "rfq-2", "market_ticker": "MKT-2"}, + ], +} + +_BATCH_URL = "https://test.kalshi.com/trade-api/v2/communications/rfq_batches" + +_CHILDREN = [ + CreateRFQBatchChild(market_ticker="MKT-1"), + CreateRFQBatchChild(market_ticker="MKT-2"), +] + + +class TestRFQBatchModels: + def test_rfq_batch_parses(self) -> None: + batch = RFQBatch.model_validate(_MINIMAL_RFQ_BATCH) + assert batch.id == "batch-1" + assert len(batch.rfqs) == 2 + assert isinstance(batch.rfqs[0], RFQ) + + def test_get_rfq_batch_response_wraps(self) -> None: + resp = GetRFQBatchResponse.model_validate({"rfq_batch": _MINIMAL_RFQ_BATCH}) + assert resp.rfq_batch.id == "batch-1" + + def test_batch_quote_parses(self) -> None: + q = RFQBatchQuote.model_validate(_MINIMAL_BATCH_QUOTE) + assert q.id == "bq-1" + assert isinstance(q.quotes[0], Quote) + + def test_best_quote_accepts_dollars_and_fp_aliases(self) -> None: + bq = RFQBatchBestQuote.model_validate( + { + "rfq_id": "rfq-1", + "market_ticker": "MKT-1", + "yes_bid_dollars": "0.5600", + "yes_contracts_offered_fp": "10.00", + } + ) + assert bq.yes_bid == Decimal("0.5600") + assert bq.yes_contracts_offered == Decimal("10.00") + + def test_best_quote_accepts_short_names(self) -> None: + bq = RFQBatchBestQuote.model_validate( + { + "rfq_id": "rfq-1", + "market_ticker": "MKT-1", + "yes_bid": "0.50", + "yes_contracts_offered": "5", + } + ) + assert bq.yes_bid == Decimal("0.50") + assert bq.yes_contracts_offered == Decimal("5") + + def test_create_rfq_batch_request_serializes_fp_and_dollars(self) -> None: + req = CreateRFQBatchRequest( + children=_CHILDREN, + contracts=Decimal("10.00"), + target_cost=Decimal("5.00"), + rest_remainder=True, + ) + body = req.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 + assert body["rest_remainder"] is True + + def test_create_rfq_batch_request_omits_optional(self) -> None: + req = CreateRFQBatchRequest(children=_CHILDREN) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert body == { + "children": [{"market_ticker": "MKT-1"}, {"market_ticker": "MKT-2"}], + } + + def test_create_rfq_batch_request_forbids_extra(self) -> None: + with pytest.raises(ValidationError): + CreateRFQBatchRequest(children=_CHILDREN, phantom=1) # type: ignore[call-arg] + + 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_quote_request_serializes_bids_without_suffix(self) -> None: + req = CreateRFQBatchQuoteRequest( + quotes=[ + RFQBatchQuoteEntry( + rfq_id="rfq-1", yes_bid=Decimal("0.56"), no_bid=Decimal("0.44") + ), + RFQBatchQuoteEntry( + rfq_id="rfq-2", yes_bid=Decimal("0.50"), no_bid=Decimal("0.50") + ), + ], + rest_remainder=True, + ) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert body["quotes"][0] == { + "rfq_id": "rfq-1", + "yes_bid": "0.56", + "no_bid": "0.44", + } + assert "yes_bid_dollars" not in body["quotes"][0] + + def test_create_rfq_batch_quote_request_forbids_extra(self) -> None: + with pytest.raises(ValidationError): + CreateRFQBatchQuoteRequest( # type: ignore[call-arg] + quotes=[ + RFQBatchQuoteEntry( + rfq_id="rfq-1", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + RFQBatchQuoteEntry( + rfq_id="rfq-2", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + ], + rest_remainder=True, + phantom=True, + ) + + def test_accept_rfq_batch_request_serializes_bid_dollars(self) -> None: + req = AcceptRFQBatchRequest( + accepted_side="yes", + min_bids=[ + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.56")), + RFQBatchAcceptLimit(rfq_id="rfq-2", bid=Decimal("0.44")), + ], + ) + body = req.model_dump(exclude_none=True, by_alias=True, mode="json") + assert body == { + "accepted_side": "yes", + "min_bids": [ + {"rfq_id": "rfq-1", "bid_dollars": "0.56"}, + {"rfq_id": "rfq-2", "bid_dollars": "0.44"}, + ], + } + + def test_accept_rfq_batch_request_forbids_extra(self) -> None: + with pytest.raises(ValidationError): + AcceptRFQBatchRequest( # type: ignore[call-arg] + accepted_side="yes", + min_bids=[ + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.5")), + RFQBatchAcceptLimit(rfq_id="rfq-2", bid=Decimal("0.5")), + ], + phantom=1, + ) + + def test_rfq_batch_accept_limit_forbids_extra(self) -> None: + with pytest.raises(ValidationError): + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.5"), phantom=1) # type: ignore[call-arg] + + +class TestListRFQBatches: + @respx.mock + def test_returns_paged_batches(self, comms: CommunicationsResource) -> None: + respx.get(_BATCH_URL).mock( + return_value=httpx.Response(200, json={"rfq_batches": [_MINIMAL_RFQ_BATCH]}) + ) + page = comms.rfq_batches.list() + assert len(page.items) == 1 + assert isinstance(page.items[0], RFQBatch) + assert page.items[0].id == "batch-1" + assert isinstance(comms.rfq_batches, RFQBatchesResource) + + @respx.mock + def test_passes_filter_params(self, comms: CommunicationsResource) -> None: + route = respx.get(_BATCH_URL).mock( + return_value=httpx.Response(200, json={"rfq_batches": []}) + ) + comms.rfq_batches.list(status="open", subaccount=3, user_filter="self", limit=10) + params = route.calls[0].request.url.params + assert params["status"] == "open" + assert params["subaccount"] == "3" + assert params["user_filter"] == "self" + assert params["limit"] == "10" + + def test_limit_out_of_range(self, comms: CommunicationsResource) -> None: + with pytest.raises(ValueError, match="limit"): + comms.rfq_batches.list(limit=21) + + @respx.mock + def test_list_all_auto_paginates(self, comms: CommunicationsResource) -> None: + respx.get(_BATCH_URL).mock( + side_effect=[ + httpx.Response( + 200, json={"rfq_batches": [_MINIMAL_RFQ_BATCH], "cursor": "page2"} + ), + httpx.Response( + 200, json={"rfq_batches": [{**_MINIMAL_RFQ_BATCH, "id": "batch-2"}]} + ), + ] + ) + ids = [b.id for b in comms.rfq_batches.list_all()] + assert ids == ["batch-1", "batch-2"] + + @respx.mock + def test_500_raises(self, comms: CommunicationsResource) -> None: + respx.get(_BATCH_URL).mock(return_value=httpx.Response(500, json={"message": "boom"})) + with pytest.raises(KalshiServerError): + comms.rfq_batches.list() + + +class TestGetRFQBatch: + @respx.mock + def test_returns_batch(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/batch-1").mock( + return_value=httpx.Response(200, json={"rfq_batch": _MINIMAL_RFQ_BATCH}) + ) + resp = comms.rfq_batches.get("batch-1") + assert resp.rfq_batch.id == "batch-1" + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/missing").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.get("missing") + + def test_empty_id_rejected(self, comms: CommunicationsResource) -> None: + with pytest.raises(ValueError, match="rfq_batch_id"): + comms.rfq_batches.get("") + + +class TestCreateRFQBatch: + @respx.mock + def test_sends_correct_body(self, comms: CommunicationsResource) -> None: + route = respx.post(_BATCH_URL).mock( + return_value=httpx.Response(201, json={"rfq_batch": _MINIMAL_RFQ_BATCH}) + ) + resp = comms.rfq_batches.create( + children=_CHILDREN, + contracts=Decimal("10"), + rest_remainder=True, + subaccount=2, + ) + assert resp.rfq_batch.id == "batch-1" + body = json.loads(route.calls[0].request.content) + assert body == { + "children": [{"market_ticker": "MKT-1"}, {"market_ticker": "MKT-2"}], + "contracts_fp": "10", + "rest_remainder": True, + "subaccount": 2, + } + + @respx.mock + def test_accepts_request_model(self, comms: CommunicationsResource) -> None: + route = respx.post(_BATCH_URL).mock( + return_value=httpx.Response(201, json={"rfq_batch": _MINIMAL_RFQ_BATCH}) + ) + req = CreateRFQBatchRequest( + children=_CHILDREN, + target_cost=Decimal("5.00"), + client_batch_id="cli-batch", + ) + comms.rfq_batches.create(request=req) + body = json.loads(route.calls[0].request.content) + assert body["target_cost_dollars"] == "5.00" + assert body["client_batch_id"] == "cli-batch" + + def test_create_rejects_request_with_kwargs(self, comms: CommunicationsResource) -> None: + req = CreateRFQBatchRequest(children=_CHILDREN) + with pytest.raises(TypeError): + comms.rfq_batches.create(request=req, children=_CHILDREN) + + def test_create_requires_children(self, comms: CommunicationsResource) -> None: + with pytest.raises(TypeError, match="children"): + comms.rfq_batches.create() + + @respx.mock + def test_400_maps_to_validation_error(self, comms: CommunicationsResource) -> None: + respx.post(_BATCH_URL).mock(return_value=httpx.Response(400, json={"message": "bad"})) + with pytest.raises(KalshiValidationError): + comms.rfq_batches.create(children=_CHILDREN) + + +class TestDeleteRFQBatch: + @respx.mock + def test_sends_delete(self, comms: CommunicationsResource) -> None: + route = respx.delete(f"{_BATCH_URL}/batch-1").mock(return_value=httpx.Response(204)) + comms.rfq_batches.delete("batch-1") + assert route.called + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.delete(f"{_BATCH_URL}/gone").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.delete("gone") + + +class TestAcceptRFQBatch: + @respx.mock + def test_sends_put_with_body(self, comms: CommunicationsResource) -> None: + route = respx.put(f"{_BATCH_URL}/batch-1/accept").mock( + return_value=httpx.Response( + 200, + json={"accepted_side": "yes", "quotes": [_MINIMAL_QUOTE, _MINIMAL_QUOTE]}, + ) + ) + resp = comms.rfq_batches.accept( + "batch-1", + accepted_side="yes", + min_bids=[ + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.56")), + RFQBatchAcceptLimit(rfq_id="rfq-2", bid=Decimal("0.44")), + ], + ) + assert resp.accepted_side == "yes" + assert len(resp.quotes) == 2 + body = json.loads(route.calls[0].request.content) + assert body["accepted_side"] == "yes" + assert body["min_bids"][0]["bid_dollars"] == "0.56" + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.put(f"{_BATCH_URL}/gone/accept").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.accept( + "gone", + accepted_side="no", + min_bids=[ + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.5")), + RFQBatchAcceptLimit(rfq_id="rfq-2", bid=Decimal("0.5")), + ], + ) + + def test_accept_requires_min_bids(self, comms: CommunicationsResource) -> None: + with pytest.raises(TypeError, match="min_bids"): + comms.rfq_batches.accept("batch-1", accepted_side="yes") + + +class TestBestQuotesRFQBatch: + @respx.mock + def test_returns_best_quotes(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/batch-1/best_quotes").mock( + return_value=httpx.Response(200, json={"best_quotes": _MINIMAL_BEST_QUOTES}) + ) + resp = comms.rfq_batches.best_quotes("batch-1") + assert isinstance(resp, GetRFQBatchBestQuotesResponse) + assert resp.best_quotes.version == 3 + assert resp.best_quotes.rfqs[0].yes_bid == Decimal("0.5600") + assert resp.best_quotes.rfqs[1].yes_bid is None + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/gone/best_quotes").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.best_quotes("gone") + + +class TestListRFQBatchQuotes: + @respx.mock + def test_returns_quotes(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/batch-1/quotes").mock( + return_value=httpx.Response(200, json={"batch_quotes": [_MINIMAL_BATCH_QUOTE]}) + ) + resp = comms.rfq_batches.quotes.list("batch-1") + assert isinstance(resp, GetRFQBatchQuotesResponse) + assert isinstance(comms.rfq_batches.quotes, RFQBatchQuotesResource) + assert len(resp.batch_quotes) == 1 + assert resp.batch_quotes[0].id == "bq-1" + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/gone/quotes").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.quotes.list("gone") + + @respx.mock + def test_empty_list(self, comms: CommunicationsResource) -> None: + respx.get(f"{_BATCH_URL}/batch-1/quotes").mock( + return_value=httpx.Response(200, json={"batch_quotes": []}) + ) + resp = comms.rfq_batches.quotes.list("batch-1") + assert resp.batch_quotes == [] + + +class TestCreateRFQBatchQuote: + @respx.mock + def test_sends_correct_body(self, comms: CommunicationsResource) -> None: + route = respx.post(f"{_BATCH_URL}/batch-1/quotes").mock( + return_value=httpx.Response(201, json={"id": "bq-new"}) + ) + resp = comms.rfq_batches.quotes.create( + "batch-1", + quotes=[ + RFQBatchQuoteEntry( + rfq_id="rfq-1", yes_bid=Decimal("0.56"), no_bid=Decimal("0.44") + ), + RFQBatchQuoteEntry( + rfq_id="rfq-2", yes_bid=Decimal("0.50"), no_bid=Decimal("0.50") + ), + ], + rest_remainder=True, + post_only=True, + ) + assert resp.id == "bq-new" + body = json.loads(route.calls[0].request.content) + assert body["rest_remainder"] is True + assert body["post_only"] is True + assert body["quotes"][0]["yes_bid"] == "0.56" + + @respx.mock + def test_400(self, comms: CommunicationsResource) -> None: + respx.post(f"{_BATCH_URL}/batch-1/quotes").mock( + return_value=httpx.Response(400, json={"message": "bad"}) + ) + with pytest.raises(KalshiValidationError): + comms.rfq_batches.quotes.create( + "batch-1", + quotes=[ + RFQBatchQuoteEntry( + rfq_id="rfq-1", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + RFQBatchQuoteEntry( + rfq_id="rfq-2", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + ], + rest_remainder=False, + ) + + def test_create_requires_quotes(self, comms: CommunicationsResource) -> None: + with pytest.raises(TypeError, match="quotes"): + comms.rfq_batches.quotes.create("batch-1", rest_remainder=True) + + +class TestDeleteRFQBatchQuote: + @respx.mock + def test_sends_delete(self, comms: CommunicationsResource) -> None: + route = respx.delete(f"{_BATCH_URL}/batch-1/quotes/bq-1").mock( + return_value=httpx.Response(204) + ) + comms.rfq_batches.quotes.delete("batch-1", "bq-1") + assert route.called + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.delete(f"{_BATCH_URL}/batch-1/quotes/gone").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.quotes.delete("batch-1", "gone") + + +class TestConfirmRFQBatchQuote: + @respx.mock + def test_sends_put_with_empty_body(self, comms: CommunicationsResource) -> None: + route = respx.put(f"{_BATCH_URL}/batch-1/quotes/bq-1/confirm").mock( + return_value=httpx.Response(204) + ) + comms.rfq_batches.quotes.confirm("batch-1", "bq-1") + assert route.called + assert route.calls[0].request.content == b"{}" + + @respx.mock + def test_404(self, comms: CommunicationsResource) -> None: + respx.put(f"{_BATCH_URL}/batch-1/quotes/gone/confirm").mock( + return_value=httpx.Response(404, json={"message": "missing"}) + ) + with pytest.raises(KalshiNotFoundError): + comms.rfq_batches.quotes.confirm("batch-1", "gone") + + +class TestAsyncRFQBatches: + async def test_list( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get(_BATCH_URL).mock( + return_value=httpx.Response(200, json={"rfq_batches": [_MINIMAL_RFQ_BATCH]}) + ) + page = await async_comms.rfq_batches.list() + assert len(page.items) == 1 + assert isinstance(page.items[0], RFQBatch) + + async def test_list_all( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get(_BATCH_URL).mock( + side_effect=[ + httpx.Response( + 200, json={"rfq_batches": [_MINIMAL_RFQ_BATCH], "cursor": "page2"} + ), + httpx.Response( + 200, json={"rfq_batches": [{**_MINIMAL_RFQ_BATCH, "id": "batch-2"}]} + ), + ] + ) + ids = [b.id async for b in async_comms.rfq_batches.list_all()] + assert ids == ["batch-1", "batch-2"] + + async def test_get( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get(f"{_BATCH_URL}/batch-1").mock( + return_value=httpx.Response(200, json={"rfq_batch": _MINIMAL_RFQ_BATCH}) + ) + resp = await async_comms.rfq_batches.get("batch-1") + assert resp.rfq_batch.id == "batch-1" + + async def test_create( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.post(_BATCH_URL).mock( + return_value=httpx.Response(201, json={"rfq_batch": _MINIMAL_RFQ_BATCH}) + ) + resp = await async_comms.rfq_batches.create(children=_CHILDREN) + assert resp.rfq_batch.id == "batch-1" + assert route.called + + async def test_delete( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.delete(f"{_BATCH_URL}/batch-1").mock( + return_value=httpx.Response(204) + ) + await async_comms.rfq_batches.delete("batch-1") + assert route.called + + async def test_accept( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.put(f"{_BATCH_URL}/batch-1/accept").mock( + return_value=httpx.Response( + 200, json={"accepted_side": "yes", "quotes": [_MINIMAL_QUOTE]} + ) + ) + resp = await async_comms.rfq_batches.accept( + "batch-1", + accepted_side="yes", + min_bids=[ + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.56")), + RFQBatchAcceptLimit(rfq_id="rfq-2", bid=Decimal("0.44")), + ], + ) + assert resp.accepted_side == "yes" + + async def test_best_quotes( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get(f"{_BATCH_URL}/batch-1/best_quotes").mock( + return_value=httpx.Response(200, json={"best_quotes": _MINIMAL_BEST_QUOTES}) + ) + resp = await async_comms.rfq_batches.best_quotes("batch-1") + assert resp.best_quotes.yes_complete is True + + async def test_list_quotes( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get(f"{_BATCH_URL}/batch-1/quotes").mock( + return_value=httpx.Response(200, json={"batch_quotes": [_MINIMAL_BATCH_QUOTE]}) + ) + resp = await async_comms.rfq_batches.quotes.list("batch-1") + assert resp.batch_quotes[0].id == "bq-1" + + async def test_create_quote( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.post(f"{_BATCH_URL}/batch-1/quotes").mock( + return_value=httpx.Response(201, json={"id": "bq-9"}) + ) + resp = await async_comms.rfq_batches.quotes.create( + "batch-1", + quotes=[ + RFQBatchQuoteEntry( + rfq_id="rfq-1", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + RFQBatchQuoteEntry( + rfq_id="rfq-2", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + ], + rest_remainder=False, + ) + assert resp.id == "bq-9" + assert route.called + + async def test_delete_quote( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.delete(f"{_BATCH_URL}/batch-1/quotes/bq-1").mock( + return_value=httpx.Response(204) + ) + await async_comms.rfq_batches.quotes.delete("batch-1", "bq-1") + assert route.called + + async def test_confirm_quote( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.put(f"{_BATCH_URL}/batch-1/quotes/bq-1/confirm").mock( + return_value=httpx.Response(204) + ) + await async_comms.rfq_batches.quotes.confirm("batch-1", "bq-1") + assert route.calls[0].request.content == b"{}" + + async def test_list_500( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get(_BATCH_URL).mock( + return_value=httpx.Response(500, json={"message": "boom"}) + ) + with pytest.raises(KalshiServerError): + await async_comms.rfq_batches.list() + + +class TestRFQBatchesAuthGuard: + def test_list_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.list() + + def test_list_all_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + list(unauth_comms.rfq_batches.list_all()) + + def test_get_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.get("batch-1") + + def test_create_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.create(children=_CHILDREN) + + def test_delete_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.delete("batch-1") + + def test_accept_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.accept( + "batch-1", + accepted_side="yes", + min_bids=[ + RFQBatchAcceptLimit(rfq_id="rfq-1", bid=Decimal("0.5")), + RFQBatchAcceptLimit(rfq_id="rfq-2", bid=Decimal("0.5")), + ], + ) + + def test_best_quotes_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.best_quotes("batch-1") + + def test_list_quotes_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.quotes.list("batch-1") + + def test_create_quote_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.quotes.create( + "batch-1", + quotes=[ + RFQBatchQuoteEntry( + rfq_id="rfq-1", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + RFQBatchQuoteEntry( + rfq_id="rfq-2", yes_bid=Decimal("0.5"), no_bid=Decimal("0.5") + ), + ], + rest_remainder=True, + ) + + def test_delete_quote_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.quotes.delete("batch-1", "bq-1") + + def test_confirm_quote_requires_auth(self, unauth_comms: CommunicationsResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.rfq_batches.quotes.confirm("batch-1", "bq-1") diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 8a1b1740..c90c8da6 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -1436,6 +1436,15 @@ def _assert_params_match( "#/components/schemas/AcceptBlockTradeProposalRequest": ( "kalshi.models.communications.AcceptBlockTradeProposalRequest" ), + "#/components/schemas/CreateRFQBatchRequest": ( + "kalshi.models.communications.CreateRFQBatchRequest" + ), + "#/components/schemas/CreateRFQBatchQuoteRequest": ( + "kalshi.models.communications.CreateRFQBatchQuoteRequest" + ), + "#/components/schemas/AcceptRFQBatchRequest": ( + "kalshi.models.communications.AcceptRFQBatchRequest" + ), "#/components/schemas/ApplySubaccountTransferRequest": ( "kalshi.models.subaccounts.ApplySubaccountTransferRequest" ), diff --git a/tests/test_model_extra_policy.py b/tests/test_model_extra_policy.py index b0a72d42..bcdef581 100644 --- a/tests/test_model_extra_policy.py +++ b/tests/test_model_extra_policy.py @@ -19,6 +19,14 @@ # request-body sub-model whose name doesn't end in plain `Request`. # `Input` covers nested write models such as `TargetBalanceAllocationInput`. _REQUEST_BODY_SUFFIXES = ("Request", "RequestOrder", "Input") +# Nested request-body items whose names don't match the suffixes above. +_NESTED_REQUEST_MODELS = frozenset( + { + "CreateRFQBatchChild", + "RFQBatchQuoteEntry", + "RFQBatchAcceptLimit", + } +) def _exported_model_classes() -> list[tuple[str, type[BaseModel]]]: @@ -39,7 +47,9 @@ def _exported_model_classes() -> list[tuple[str, type[BaseModel]]]: def _is_request_body(name: str) -> bool: - return any(name.endswith(suffix) for suffix in _REQUEST_BODY_SUFFIXES) + return name in _NESTED_REQUEST_MODELS or any( + name.endswith(suffix) for suffix in _REQUEST_BODY_SUFFIXES + ) _ALL_MODELS = _exported_model_classes() diff --git a/tests/ws/test_models.py b/tests/ws/test_models.py index 15d2a16f..5d30b376 100644 --- a/tests/ws/test_models.py +++ b/tests/ws/test_models.py @@ -846,6 +846,23 @@ def test_quote_executed_payload_model(self) -> None: ) assert payload.order_id == "ord-001" assert payload.executed_ts == datetime(2026, 4, 19, 18, 43, 37, tzinfo=UTC) + assert payload.rfq_batch_id is None + + def test_quote_executed_payload_rfq_batch_id(self) -> None: + payload = QuoteExecutedPayload.model_validate( + { + "quote_id": "q-001", + "rfq_id": "rfq-001", + "order_id": "ord-001", + "quote_creator_id": "user-2", + "rfq_creator_id": "user-1", + "client_order_id": "cli-001", + "market_ticker": "T", + "executed_ts": "2026-04-19T18:43:37Z", + "rfq_batch_id": "batch-1", + } + ) + assert payload.rfq_batch_id == "batch-1" # ---------- v0.14+ backfill (#162): one (de)serialization test per payload ----------