diff --git a/CHANGELOG.md b/CHANGELOG.md index f530f70..99272d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ All notable changes to kalshi-sdk will be documented in this file. +## 5.0.0 — 2026-06-26 + +Syncs the upstream OpenAPI spec **3.21.0 → 3.22.0** (#454, #458). The headline +change is **breaking**: Kalshi removed the V1 order-write endpoints from the +spec, so the SDK removes the V1 order methods and their models. Order writes now +go exclusively through the V2 `/portfolio/events/orders` family (the `*_v2` +methods, which have existed since 3.18.0). See `docs/migration.md` for the +V1 → V2 mapping. + +### Removed (breaking) + +- **V1 order-write methods** on `client.orders` (sync + async): `create`, + `cancel`, `batch_create`, `batch_cancel`, `amend`, `decrease`. The underlying + endpoints (`POST/DELETE /portfolio/orders`, `/portfolio/orders/batched`, + `/portfolio/orders/{id}/amend`, `/portfolio/orders/{id}/decrease`) were + removed from the spec in 3.22.0. Use the `*_v2` equivalents instead. +- **V1 order models** (no longer exported from `kalshi` / `kalshi.models`): + `CreateOrderRequest`, `AmendOrderRequest`, `AmendOrderResponse`, + `DecreaseOrderRequest`, `BatchCreateOrdersRequest`, `BatchCreateOrdersResponse`, + `BatchCreateOrdersResponseEntry`, `BatchCancelOrdersRequest`, + `BatchCancelOrdersResponse`, `BatchCancelOrdersResponseEntry`, + `BatchCancelOrdersRequestOrder`, and the `ActionLiteral` (`buy`/`sell`) alias. +- **Dead quote filters** — `event_ticker` and `market_ticker` were removed from + `GET /communications/quotes` upstream, so they are removed from + `client.communications.quotes.list` / `list_all` and the + `communications.list_quotes` / `list_all_quotes` facade methods. + +### Added + +- **RFQ-scoped quote actions** (spec 3.22.0) on `client.communications.quotes` + (sync + async): `accept_for_rfq(rfq_id, quote_id, ...)`, + `confirm_for_rfq(rfq_id, quote_id)`, `delete_for_rfq(rfq_id, quote_id)` — + backing `PUT/DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id}[/accept|/confirm]`. +- **`cancel_v2` `market_ticker` query param** — required when `exchange_index` + is `-1` (auto-route by ticker). +- **`BatchCancelOrdersV2RequestOrder.market_ticker`** — same auto-route semantics + for batch V2 cancels. +- **`SubaccountBalance.exchange_index`** — the exchange shard a balance is held on. +- **Perps SCM** (`kalshi.perps.klear`): new `MarketSettlementEstimate` model; + `SettlementEstimate.positions` (per-market breakdown map); and + `GetSettlementEstimateResponse.prev_settlement_prices` (market → last + settlement price, centicents). + ## 4.2.0 — 2026-06-19 Reconciles in-place upstream spec drift detected by the nightly run (#451): diff --git a/CLAUDE.md b/CLAUDE.md index dea90de..d98d39c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,7 +122,7 @@ tests/ ## API Reference -- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.21.0, 104 operations) +- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.22.0, 101 operations) - AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (13 WebSocket channels) - Base URL: https://api.elections.kalshi.com/trade-api/v2 - Demo URL: https://demo-api.kalshi.co/trade-api/v2 diff --git a/README.md b/README.md index 798ae96..2508288 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ 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 (104 operations across 19 resources, OpenAPI v3.21.0) and WebSocket API (12 typed `subscribe_*` channels + 2 escape-hatch). +- **Full coverage** of the Kalshi REST API (101 operations across 19 resources, OpenAPI v3.22.0) and WebSocket API (12 typed `subscribe_*` channels + 2 escape-hatch). - **Perps (margin) API**: standalone `PerpsClient` / `AsyncPerpsClient` + `PerpsWebSocket` for the perpetual-futures exchange (34 REST operations, 6 WS channels), plus a `KlearClient` for the Self-Clearing-Member "Klear" settlement API (9 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` plus batched variants on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` keeps working — deprecated no earlier than May 6, 2026. +- **V2 event-market orders**: `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` plus batched variants on `/portfolio/events/orders/*` — the only order-write surface. - **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`. - **Sync and async** clients sharing one transport — no thread-pool wrapping. - **Typed end-to-end**: Pydantic v2 models, `mypy --strict` clean, ships `py.typed`. `Literal` types on fixed-enum kwargs. @@ -113,44 +113,9 @@ with KalshiClient(demo=True) as client: ## Placing orders -```python -from kalshi import KalshiClient - -with KalshiClient.from_env() as client: - order = client.orders.create( - ticker="EXAMPLE-25-T", - side="yes", - action="buy", - count=10, - yes_price="0.65", # 65 cents - time_in_force="good_till_canceled", - client_order_id="my-uuid", # idempotency key - ) - print(order.order_id, order.status) -``` - -Prices are decimal dollars (e.g. `"0.65"`) per the Kalshi spec. Internally -the SDK uses `Decimal` via the `DollarDecimal` type — never `float`. - -Every POST/PUT/DELETE-with-body method also accepts a pre-built request model -as an alternative to individual kwargs (useful for programmatic order -construction): - -```python -from kalshi import CreateOrderRequest - -client.orders.create(request=CreateOrderRequest( - ticker="EXAMPLE-25-T", side="yes", action="buy", - count=10, yes_price="0.65", -)) -``` - -### V2 event-market orders - -Spec v3.18.0 introduced the V2 family on `/portfolio/events/orders/*` — -event-scoped semantics with single-book `bid`/`ask` sides and fixed-point -dollar prices. Legacy `/portfolio/orders` keeps working and will be -deprecated no earlier than May 6, 2026. +Orders are written through the V2 event-market family on +`/portfolio/events/orders/*` — event-scoped semantics with single-book +`bid`/`ask` sides and fixed-point dollar prices. ```python import uuid @@ -163,15 +128,28 @@ with KalshiClient.from_env() as client: client_order_id=str(uuid.uuid4()), # required + server idempotency key side="bid", # BookSideLiteral: "bid" | "ask" count=Decimal("10"), - price=Decimal("0.50"), + price=Decimal("0.50"), # 50 cents time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross", )) print(resp.order_id, resp.remaining_count, resp.fill_count) ``` -The V2 surface is model-only (no kwarg overload); pass a fully-constructed -request model. See [V2 orders docs](https://texascoding.github.io/kalshi-python-sdk/resources/orders/#v2-event-market-orders) for amend/decrease/batch variants. +Prices and counts are `Decimal` — never `float`. Internally the SDK uses the +`DollarDecimal` type for prices (FixedPointDollars on the wire). `side` is the +book side (`"bid"` / `"ask"`), not `"yes"` / `"no"`, and `client_order_id` is +required on `CreateOrderV2Request` (the server uses it for idempotency). + +The V2 surface is **model-only** — there is no kwarg overload. Every write +takes a fully-constructed request model whose `model_config = {"extra": +"forbid"}` rejects phantom keys at construction time. Cancel takes the order id +directly: + +```python +client.orders.cancel_v2(resp.order_id) +``` + +See [V2 orders docs](https://texascoding.github.io/kalshi-python-sdk/resources/orders/#v2-event-market-orders) for `amend_v2` / `decrease_v2` / `batch_create_v2` / `batch_cancel_v2`. ## WebSocket streaming diff --git a/docs/concepts.md b/docs/concepts.md index 2a1ae4b..a38f23c 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -17,17 +17,17 @@ The three-level ticker hierarchy: Every market belongs to exactly one event; every event belongs to exactly one series. -## YES, NO, side, action +## YES, NO, book side A Kalshi market trades two complementary contracts that always sum to $1: -**YES** and **NO**. Each trade has a `side` (which contract) and an `action`: +**YES** and **NO**. Orders are placed against a single order book, and the +`side` you specify is the **book side**, not the contract: -- `side="yes"`, `action="buy"` — buying YES. -- `side="yes"`, `action="sell"` — selling YES. -- `side="no"`, `action="buy"` — buying NO (equivalent to selling YES against - the orderbook, but accounted separately). +- `side="bid"` — you are bidding (buying into the book). +- `side="ask"` — you are asking (selling into the book). -These are `Literal` types — see [Types & literals](types.md). +`BookSideLiteral` is a `Literal["bid", "ask"]` — see +[Types & literals](types.md). ## Prices @@ -35,8 +35,10 @@ Prices live in `[0.00, 1.00]` and represent dollars (a YES at $0.65 implies a 65% market-implied probability). Always pass them as strings or `Decimal`: ```python -order = client.orders.create(..., yes_price="0.65") -order = client.orders.create(..., yes_price=Decimal("0.65")) +from kalshi.models.orders import CreateOrderV2Request + +CreateOrderV2Request(..., price="0.65") +CreateOrderV2Request(..., price=Decimal("0.65")) ``` Float is a footgun; the SDK accepts it but normalizes through `str()` to avoid @@ -74,12 +76,11 @@ separate, future change. A few fields are **integer cents**, not dollars: - `Balance.balance` / `portfolio_value` — cents. -- `CreateOrderRequest.buy_max_cost` — cents. - `ApplySubaccountTransferRequest.amount_cents` — cents. These are typed `int`. Passing a `Decimal` or `float` raises `ValueError` at -construction. The rule: anything with `_cents` / `buy_max_cost` is cents; -anything with `_dollars` or `yes_price` / `no_price` is `Decimal` dollars. +construction. The rule: anything with `_cents` is cents; anything with +`_dollars` or a `price` field is `Decimal` dollars. ## Order, fill, position, settlement @@ -89,13 +90,11 @@ anything with `_dollars` or `yes_price` / `no_price` is `Decimal` dollars. - **Position** — your aggregate exposure on a market (signed by side). - **Settlement** — what the exchange paid out when the market resolved. -Orders come in two families: - -- **V1** — `/portfolio/orders/*`. Yes/no sides, paired `yes_price` / - `no_price`. Stable surface; deprecation no earlier than May 6, 2026. -- **V2** — `/portfolio/events/orders/*`. Event-scoped, single-book - `bid` / `ask` sides, single `price` field, required `client_order_id` - acting as an idempotency key. Use this for new event-market integrations. +Orders are written through the **V2** surface — `/portfolio/events/orders/*`. +It is event-scoped with single-book `bid` / `ask` sides, a single `price` +field, and a required `client_order_id` that acts as an idempotency key. +Every write builds a request model (e.g. `CreateOrderV2Request`) — there is +no kwarg overload, so always construct the model and pass it as `request=`. See [Orders](resources/orders.md), [Portfolio](resources/portfolio.md). @@ -173,14 +172,19 @@ doesn't belong on every request (set those via ``KalshiConfig.extra_headers``). ```python import uuid - -order = client.orders.create( - ticker="KXPRES-24-DJT", - side="yes", - action="buy", - count=1, - yes_price="0.65", - client_order_id="cli-1", +from decimal import Decimal +from kalshi.models.orders import CreateOrderV2Request + +order = client.orders.create_v2( + request=CreateOrderV2Request( + ticker="KXPRES-24-DJT", + client_order_id="cli-1", + side="bid", + count=Decimal("1"), + price=Decimal("0.65"), + time_in_force="fill_or_kill", + self_trade_prevention_type="taker_at_cross", + ), extra_headers={"Idempotency-Key": str(uuid.uuid4())}, ) ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index c7031ba..d32c749 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -99,34 +99,40 @@ asyncio.run(main()) ## Place an order (demo) +Order writes go through the V2 `/portfolio/events/orders` family. There is no +keyword overload — you always build a request model and pass it as `request=`: + ```python import uuid +from decimal import Decimal from kalshi import KalshiClient +from kalshi.models import CreateOrderV2Request with KalshiClient.from_env() as client: - order = client.orders.create( - ticker="EXAMPLE-25-T", - side="yes", - action="buy", - count=10, - yes_price="0.65", # 65¢ — strings or Decimals, never float - time_in_force="good_till_canceled", - client_order_id=str(uuid.uuid4()), # idempotency key (see below) + order = client.orders.create_v2( + request=CreateOrderV2Request( + ticker="EXAMPLE-25-T", + client_order_id=str(uuid.uuid4()), # idempotency key (see below) + side="bid", # book side: "bid"/"ask", not "yes"/"no" + count=Decimal("10"), + price=Decimal("0.65"), # 65¢ — strings or Decimals, never float + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) ) - print(order.order_id, order.status) + print(order.order_id, order.fill_count, order.remaining_count) ``` !!! warning "POST is never retried automatically" The transport never retries `POST` (or `DELETE`) requests, to avoid duplicate - orders. Pass a fresh `client_order_id` on every `create()` call so you can safely + orders. Pass a fresh `client_order_id` on every `create_v2()` call so you can safely retry from your application layer without double-filling. See [Retries & idempotency](retries.md). -!!! warning "`buy_max_cost` is integer cents, not dollars" - `CreateOrderRequest.buy_max_cost` is `int` cents — `500` means $5.00. Passing a - `Decimal` or `float` raises `ValueError` at construction. Other price fields - (`yes_price`, `no_price`) are `DollarDecimal` and accept strings or - `Decimal`. +!!! note "`side` is the book side, not the outcome" + `CreateOrderV2Request.side` is `"bid"` or `"ask"` (the resting book side), not + `"yes"`/`"no"`. `client_order_id` is required on the model, and `count` and + `price` are `Decimal` — passing a `float` for a price is rejected. Prices are decimal dollars per the Kalshi spec. Internally the SDK uses `Decimal` via the [`DollarDecimal`](types.md) type — never `float`. diff --git a/docs/index.md b/docs/index.md index b5f9409..e6fa383 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** — 104 operations across 19 resources (OpenAPI v3.21.0), +- **Full REST coverage** — 101 operations across 19 resources (OpenAPI v3.22.0), every kwarg drift-tested against the spec. - **V2 event-market orders** — new `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` family on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` diff --git a/docs/migration.md b/docs/migration.md index accab8d..afda350 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,5 +1,76 @@ # Migration +## v4 → v5.0.0 + +**Breaking: the V1 order-write API is gone.** Kalshi removed the V1 order +endpoints from the OpenAPI spec in 3.22.0, so the SDK removed the matching +methods and models. Order writes now go exclusively through the V2 +`/portfolio/events/orders` family (the `*_v2` methods, available since 3.18.0). + +Read endpoints are unchanged: `orders.get`, `orders.list`, `orders.list_all`, +`orders.queue_positions`, `orders.queue_position`, and `portfolio.fills` all keep +working exactly as before. + +### Removed methods → replacement + +| Removed (v4) | Use instead (v5) | +| ------------------------------------ | -------------------------------------------------- | +| `orders.create(...)` | `orders.create_v2(request=CreateOrderV2Request(...))` | +| `orders.cancel(order_id)` | `orders.cancel_v2(order_id, ...)` | +| `orders.amend(order_id, ...)` | `orders.amend_v2(order_id, request=AmendOrderV2Request(...))` | +| `orders.decrease(order_id, ...)` | `orders.decrease_v2(order_id, request=DecreaseOrderV2Request(...))` | +| `orders.batch_create(orders=[...])` | `orders.batch_create_v2(request=BatchCreateOrdersV2Request(orders=[...]))` | +| `orders.batch_cancel(orders=[...])` | `orders.batch_cancel_v2(request=BatchCancelOrdersV2Request(orders=[...]))` | + +The V2 models use a single `price` with a `side` of `"bid"`/`"ask"` (book side), +rather than the V1 `yes_price` / `no_price` + `yes`/`no`: + +```python +from decimal import Decimal +from kalshi.models import CreateOrderV2Request + +# v4 (removed) +# order = client.orders.create(ticker="MKT-A", side="yes", action="buy", count=1, yes_price="0.56") + +# v5 +resp = client.orders.create_v2( + request=CreateOrderV2Request( + ticker="MKT-A", + client_order_id="my-idempotency-key", + side="bid", # book side, not yes/no + count=Decimal("1"), + price=Decimal("0.56"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) +) +``` + +### Removed models + +These are no longer exported from `kalshi` / `kalshi.models`: +`CreateOrderRequest`, `AmendOrderRequest`, `AmendOrderResponse`, +`DecreaseOrderRequest`, `BatchCreateOrdersRequest`, `BatchCreateOrdersResponse`, +`BatchCreateOrdersResponseEntry`, `BatchCancelOrdersRequest`, +`BatchCancelOrdersResponse`, `BatchCancelOrdersResponseEntry`, +`BatchCancelOrdersRequestOrder`, and the `ActionLiteral` alias. Use the `…V2…` +models instead. + +### Other breaking changes + +- `communications.quotes.list` / `list_all` (and the `communications.list_quotes` + / `list_all_quotes` facades) no longer accept `event_ticker` / `market_ticker` + — those filters were removed from `GET /communications/quotes` upstream. + +### New in 5.0.0 + +- RFQ-scoped quote actions: `communications.quotes.accept_for_rfq`, + `confirm_for_rfq`, and `delete_for_rfq` (take both `rfq_id` and `quote_id`). +- `cancel_v2(..., market_ticker=...)` and `BatchCancelOrdersV2RequestOrder.market_ticker` + for `-1` (auto-route) exchange indices. +- `SubaccountBalance.exchange_index`; perps SCM `MarketSettlementEstimate`, + `SettlementEstimate.positions`, `GetSettlementEstimateResponse.prev_settlement_prices`. + ## v3 → v4.0.0 v4.0.0 has **one breaking change** — the Self-Clearing-Member "Klear" API diff --git a/docs/reference.md b/docs/reference.md index 2c6fafc..5efb63a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -34,17 +34,17 @@ every exception class. ## Request models -::: kalshi.models.orders.CreateOrderRequest +::: kalshi.models.orders.CreateOrderV2Request -::: kalshi.models.orders.AmendOrderRequest +::: kalshi.models.orders.AmendOrderV2Request -::: kalshi.models.orders.DecreaseOrderRequest +::: kalshi.models.orders.DecreaseOrderV2Request -::: kalshi.models.orders.BatchCreateOrdersRequest +::: kalshi.models.orders.BatchCreateOrdersV2Request -::: kalshi.models.orders.BatchCancelOrdersRequest +::: kalshi.models.orders.BatchCancelOrdersV2Request -::: kalshi.models.orders.BatchCancelOrdersRequestOrder +::: kalshi.models.orders.BatchCancelOrdersV2RequestOrder ::: kalshi.models.api_keys.CreateApiKeyRequest @@ -88,7 +88,7 @@ every exception class. ::: kalshi.models.orders.Fill -::: kalshi.models.orders.AmendOrderResponse +::: kalshi.models.orders.AmendOrderV2Response ::: kalshi.models.orders.OrderQueuePosition diff --git a/docs/request-models.md b/docs/request-models.md index c38b52f..9451f3f 100644 --- a/docs/request-models.md +++ b/docs/request-models.md @@ -1,40 +1,32 @@ # Request models -Every mutating endpoint (`POST`, `PUT`, `DELETE` with a body) accepts its -parameters two ways: as individual keyword arguments or as a pre-built request -model. +Every mutating endpoint (`POST`, `PUT`, `DELETE` with a body) takes its +parameters as a pre-built request model. You construct the model, then pass it +as `request=`. -## The two forms +## The request-model form ```python -# Form 1 — individual kwargs (default for most call sites) -order = client.orders.create( - ticker="KXPRES-24-DJT", - side="yes", - action="buy", - count=10, - yes_price="0.65", - time_in_force="good_till_canceled", -) - -# Form 2 — pass a pre-built request model -from kalshi import CreateOrderRequest +import uuid +from decimal import Decimal +from kalshi import CreateOrderV2Request -req = CreateOrderRequest( +req = CreateOrderV2Request( ticker="KXPRES-24-DJT", - side="yes", - action="buy", - count=10, - yes_price="0.65", + client_order_id=str(uuid.uuid4()), + side="bid", + count=Decimal("10"), + price=Decimal("0.65"), time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", ) -order = client.orders.create(request=req) +order = client.orders.create_v2(request=req) ``` -The two forms are **mutually exclusive** — passing `request=` together with -any other kwarg raises `TypeError`. The request-model form is useful when you -build orders out of band (config, queue, test harness) and want to type-check -the whole payload at construction time. +The request-model form lets you build orders out of band (config, queue, test +harness) and type-check the whole payload at construction time. Prices and +counts are `Decimal`; `side` is the book side (`"bid"` / `"ask"`, not +`"yes"` / `"no"`); `client_order_id` is required. ## `extra="forbid"` everywhere @@ -43,30 +35,25 @@ removed field fails at construction with a Pydantic `ValidationError` — **before** the HTTP request goes out. ```python -from kalshi import CreateOrderRequest +from kalshi import CreateOrderV2Request -CreateOrderRequest( - ticker="X", side="yes", action="buy", count=1, yes_price="0.65", +CreateOrderV2Request( + ticker="X", client_order_id="abc", side="bid", count=1, price="0.65", + time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross", typo_field=123, # raises ValidationError immediately ) ``` -The same models drive the body-drift contract tests in CI, so the kwargs -exposed on each resource method and the wire format stay in lockstep with the -OpenAPI spec. +The same models drive the body-drift contract tests in CI, so the wire format +exposed by each resource method stays in lockstep with the OpenAPI spec. ## Inventory | Resource | Request model | |---|---| -| `client.orders.create` | `CreateOrderRequest` | -| `client.orders.batch_create` | `BatchCreateOrdersRequest` (wraps `list[CreateOrderRequest]`) | -| `client.orders.batch_cancel` | `BatchCancelOrdersRequest` (wraps `list[BatchCancelOrdersRequestOrder]`) | -| `client.orders.amend` | `AmendOrderRequest` | -| `client.orders.decrease` | `DecreaseOrderRequest` | -| `client.orders.create_v2` | `CreateOrderV2Request` (V2 event-market — model-only, no kwarg form) | -| `client.orders.amend_v2` | `AmendOrderV2Request` (V2 — model-only) | -| `client.orders.decrease_v2` | `DecreaseOrderV2Request` (V2 — model-only, XOR `reduce_by`/`reduce_to`) | +| `client.orders.create_v2` | `CreateOrderV2Request` | +| `client.orders.amend_v2` | `AmendOrderV2Request` | +| `client.orders.decrease_v2` | `DecreaseOrderV2Request` (XOR `reduce_by` / `reduce_to`) | | `client.orders.batch_create_v2` | `BatchCreateOrdersV2Request` (wraps `list[CreateOrderV2Request]`) | | `client.orders.batch_cancel_v2` | `BatchCancelOrdersV2Request` (wraps `list[BatchCancelOrdersV2RequestOrder]`) | | `client.api_keys.create` | `CreateApiKeyRequest` | @@ -92,22 +79,16 @@ an awkward wire name: | Model field | Wire name | |---|---| -| `CreateOrderRequest.count` | `count_fp` | -| `CreateOrderRequest.yes_price` | `yes_price_dollars` | -| `CreateOrderRequest.no_price` | `no_price_dollars` | -| `AmendOrderRequest.yes_price` / `.no_price` / `.count` | `*_dollars` / `count_fp` | -| `CreateQuoteRequest.target_cost` | `target_cost_dollars` | +| `CreateRFQRequest.target_cost` | `target_cost_dollars` | | `StructuredTarget.target_type` (response) | `type` (avoids shadowing the builtin) | You never type these long names — they're an internal implementation detail. -## V2 surface: model-only +## V2 idempotency keys -The V2 event-market order endpoints (`create_v2` / `amend_v2` / `decrease_v2` -/ `batch_*_v2`) don't accept individual kwargs — pass a fully-constructed -request model. This is intentional: V2 took the opportunity to drop the -V1 kwarg-overload surface entirely, so the model is the single source of -truth for the payload shape. +`CreateOrderV2Request.client_order_id` is required and the server treats it as +an idempotency key — reusing one returns the original order rather than placing +a new one. Generate a fresh UUID4 per call. ```python import uuid @@ -126,19 +107,12 @@ req = CreateOrderV2Request( client.orders.create_v2(request=req) ``` -`CreateOrderV2Request.client_order_id` is required (V1's was optional) and -the server treats it as an idempotency key — reusing one returns the -original order rather than placing a new one. Generate a fresh UUID4 per -call. - ## Cross-field invariants Some request models enforce relationships **before** the HTTP call: -- `DecreaseOrderRequest` / `DecreaseOrderV2Request` — exactly one of - `reduce_by` / `reduce_to` is required (XOR enforced via a model validator). -- `AmendOrderRequest` — at least one of `yes_price` / `no_price` / `count` - must be present (enforced in the resource method, before the body is built). +- `DecreaseOrderV2Request` — exactly one of `reduce_by` / `reduce_to` is + required (XOR enforced via a model validator). Bad input fails locally with a clear traceback; no wasted round trip. @@ -148,12 +122,19 @@ If you need to inspect or mutate the wire body, request models serialize like this: ```python -from kalshi import CreateOrderRequest +from decimal import Decimal +from kalshi import CreateOrderV2Request -req = CreateOrderRequest(ticker="X", side="yes", action="buy", count=1, yes_price="0.65") +req = CreateOrderV2Request( + ticker="X", client_order_id="abc", side="bid", + count=Decimal("1"), price=Decimal("0.65"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", +) body = req.model_dump(exclude_none=True, by_alias=True, mode="json") -# {'ticker': 'X', 'side': 'yes', 'action': 'buy', 'count_fp': '1', -# 'yes_price_dollars': '0.65'} +# {'ticker': 'X', 'client_order_id': 'abc', 'side': 'bid', 'count': '1', +# 'price': '0.65', 'time_in_force': 'good_till_canceled', +# 'self_trade_prevention_type': 'taker_at_cross'} ``` `exclude_none=True` and `by_alias=True` are exactly what every resource method diff --git a/docs/resources/order-groups.md b/docs/resources/order-groups.md index ec7e177..720d21d 100644 --- a/docs/resources/order-groups.md +++ b/docs/resources/order-groups.md @@ -24,16 +24,26 @@ Auth required throughout. ## Lifecycle ```python -from kalshi import CreateOrderRequest +from decimal import Decimal + +from kalshi import CreateOrderV2Request # 1) Create a group with a 100-contract cap. group = client.order_groups.create(contracts_limit=100) print(group.order_group_id) -# 2) Attach orders by id. -client.orders.create( - ticker="KXPRES-24-DJT", side="yes", action="buy", count=10, - yes_price="0.65", order_group_id=group.order_group_id, +# 2) Attach orders by passing the group id on the order request model. +client.orders.create_v2( + request=CreateOrderV2Request( + ticker="KXPRES-24-DJT", + client_order_id="og-attach-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.65"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + order_group_id=group.order_group_id, + ) ) # 3) Inspect. diff --git a/docs/resources/orders.md b/docs/resources/orders.md index 01f4c75..013745e 100644 --- a/docs/resources/orders.md +++ b/docs/resources/orders.md @@ -7,69 +7,54 @@ Every method here requires auth. ## Quick reference -### V1 (legacy `/portfolio/orders/*`) — deprecated no earlier than May 6, 2026 +Order writes go through the V2 event-market family (`/portfolio/events/orders/*`). +Reads stay on `/portfolio/orders/*`. | Method | Endpoint | Retry | |---|---|---| -| `create(...)` | `POST /portfolio/orders` | **never** — see [Retries & idempotency](../retries.md) | -| `batch_create(orders)` | `POST /portfolio/orders/batched` | never | +| `create_v2(*, request)` | `POST /portfolio/events/orders` | **never** — see [Retries & idempotency](../retries.md) | +| `batch_create_v2(*, request)` | `POST /portfolio/events/orders/batched` | never | +| `cancel_v2(order_id, *, subaccount, exchange_index, market_ticker)` | `DELETE /portfolio/events/orders/{order_id}` | never | +| `batch_cancel_v2(*, request)` | `DELETE /portfolio/events/orders/batched` | never | +| `amend_v2(order_id, *, request, subaccount)` | `POST /portfolio/events/orders/{order_id}/amend` | never | +| `decrease_v2(order_id, *, request, subaccount)` | `POST /portfolio/events/orders/{order_id}/decrease` | never | | `get(order_id)` | `GET /portfolio/orders/{order_id}` | yes (GET) | | `list(...)` / `list_all(...)` | `GET /portfolio/orders` | yes | -| `cancel(order_id, *, subaccount=None, exchange_index=None)` | `DELETE /portfolio/orders/{order_id}` | never | -| `batch_cancel(orders)` | `DELETE /portfolio/orders/batched` | never | -| `amend(order_id, ...)` | `POST /portfolio/orders/{order_id}/amend` | never | -| `decrease(order_id, *, reduce_by, reduce_to)` | `POST /portfolio/orders/{order_id}/decrease` | never | | ~~`fills(...)` / `fills_all(...)`~~ | moved to `PortfolioResource` in v3.0.0 — see [Portfolio › Fills](portfolio.md#fills); the old methods remain as deprecated aliases until removal in a future release. | | `queue_positions(*, market_tickers, event_ticker)` | `GET /portfolio/orders/queue_positions` | yes | | `queue_position(order_id)` | `GET /portfolio/orders/{order_id}/queue_position` | yes | -### V2 event-market orders (`/portfolio/events/orders/*`) — recommended for new code - -| Method | Endpoint | Retry | -|---|---|---| -| `create_v2(*, request)` | `POST /portfolio/events/orders` | never | -| `batch_create_v2(*, request)` | `POST /portfolio/events/orders/batched` | never | -| `cancel_v2(order_id, *, subaccount, exchange_index)` | `DELETE /portfolio/events/orders/{order_id}` | never | -| `batch_cancel_v2(*, request)` | `DELETE /portfolio/events/orders/batched` | never | -| `amend_v2(order_id, *, request, subaccount)` | `POST /portfolio/events/orders/{order_id}/amend` | never | -| `decrease_v2(order_id, *, request, subaccount)` | `POST /portfolio/events/orders/{order_id}/decrease` | never | - V2 uses `BookSideLiteral` (`"bid"` / `"ask"`), a single `price` field (not paired yes/no), and requires `client_order_id` as a server-side -idempotency key. The surface is **model-only** — every method takes a +idempotency key. The surface is **model-only** — every write method takes a pre-built request model rather than kwargs. ## Place an order ```python import uuid -from kalshi import KalshiClient +from decimal import Decimal +from kalshi import KalshiClient, CreateOrderV2Request with KalshiClient.from_env() as client: - order = client.orders.create( + resp = client.orders.create_v2(request=CreateOrderV2Request( ticker="KXPRES-24-DJT", - side="yes", # SideLiteral - action="buy", # ActionLiteral (required) - count=10, - yes_price="0.65", # str or Decimal — never float - client_order_id=str(uuid.uuid4()), + client_order_id=str(uuid.uuid4()), # required — idempotency key + side="bid", # BookSideLiteral — "bid" / "ask" + count=Decimal("10"), # Decimal — never float + price=Decimal("0.65"), # Decimal dollars — never float time_in_force="good_till_canceled", # TimeInForceLiteral - post_only=False, - reduce_only=False, self_trade_prevention_type="taker_at_cross", - cancel_order_on_pause=False, - # buy_max_cost=500, # integer cents — $5.00 limit. Not dollars. - # expiration_ts=1_800_000_000, # Unix seconds - # order_group_id="og_abc", # attach to an order group - # subaccount=1, # route to subaccount 1 - ) - print(order.order_id, order.status) + # subaccount=1, # route to subaccount 1 + # exchange_index=0, + )) + print(resp.order_id, resp.remaining_count, resp.fill_count) ``` -### Limit vs market - -A market order is just a `create()` with no `yes_price` / `no_price`. The -order type is server-derived from price presence. +The write surface is **model-only**: there is no kwarg overload. Always build a +`CreateOrderV2Request`. `side` is the book side `"bid"` / `"ask"` (not +`"yes"` / `"no"`), `count` and `price` are `Decimal`, and `client_order_id` +is required. ### Time-in-force @@ -79,21 +64,13 @@ order type is server-derived from price presence. | `"good_till_canceled"` | Rest until canceled or filled. Server default if you omit. | | `"immediate_or_cancel"` | Fill whatever you can immediately, cancel the rest. | -### `buy_max_cost` is integer cents - -```python -client.orders.create(..., buy_max_cost=500) # cap at $5.00 -``` - -The model rejects `Decimal` or `float` at construction. The rule: any field -whose name ends in `_cents` or that is `buy_max_cost` is `int` cents; price -fields (`yes_price`, `no_price`) are `Decimal` dollars. - ### `client_order_id` for safe retries -`POST /portfolio/orders` is **never automatically retried**. Set a fresh -`client_order_id` per call so you can safely retry from your app layer -without double-filling. See [Retries & idempotency](../retries.md). +`POST /portfolio/events/orders` is **never automatically retried**. Set a +fresh `client_order_id` per call so you can safely retry from your app layer +without double-filling. Reusing a `client_order_id` for a second call returns +the **original** order rather than placing a new one — generate a fresh UUID4 +per request. See [Retries & idempotency](../retries.md). ### Self-trade prevention @@ -105,64 +82,76 @@ without double-filling. See [Retries & idempotency](../retries.md). ## Batch create ```python -from kalshi import CreateOrderRequest +import uuid +from decimal import Decimal +from kalshi import BatchCreateOrdersV2Request, CreateOrderV2Request -orders = [ - CreateOrderRequest(ticker="X-YES", side="yes", action="buy", count=10, yes_price="0.60"), - CreateOrderRequest(ticker="X-NO", side="no", action="buy", count=10, no_price="0.42"), -] -result = client.orders.batch_create(orders) +result = client.orders.batch_create_v2(request=BatchCreateOrdersV2Request( + orders=[ + CreateOrderV2Request( + ticker="X-A", client_order_id=str(uuid.uuid4()), + side="bid", count=Decimal("10"), price=Decimal("0.60"), + time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross", + ), + CreateOrderV2Request( + ticker="X-B", client_order_id=str(uuid.uuid4()), + side="ask", count=Decimal("10"), price=Decimal("0.42"), + time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross", + ), + ], +)) for entry in result.orders: if entry.error is not None: - print("failed:", entry.client_order_id, entry.error) + print("failed:", entry.error) else: - print(entry.order.order_id, entry.order.status) + print(entry.order_id, entry.fill_count) ``` -Each child has `extra="forbid"`, so a typo in any leg fails at construction -before the round trip. +Each child `CreateOrderV2Request` has `extra="forbid"`, so a typo in any leg +fails at construction before the round trip. ## Cancel ```python -client.orders.cancel("ord_abc") # single - -result = client.orders.batch_cancel(["ord_abc", "ord_def"]) # list of strings -for entry in result.orders: - if entry.error is not None: - print("failed:", entry.order_id, entry.error) - else: - print(entry.order_id, "canceled", entry.reduced_by_fp) +result = client.orders.cancel_v2("ord_abc") +print(result.reduced_by, result.ts_ms) # FixedPointCount, int ``` -For per-entry subaccount routing, build the request explicitly: +`cancel_v2` returns a `CancelOrderV2Response` (with the count actually +canceled). A 204 No Content from the server is treated as a protocol +violation and raises `KalshiError`. + +For batch cancels, build a `BatchCancelOrdersV2Request`. Each entry can carry +its own `subaccount` / `market_ticker`: ```python -from kalshi import BatchCancelOrdersRequestOrder +from kalshi import BatchCancelOrdersV2Request, BatchCancelOrdersV2RequestOrder -result = client.orders.batch_cancel([ - BatchCancelOrdersRequestOrder(order_id="ord_abc", subaccount=0), - BatchCancelOrdersRequestOrder(order_id="ord_def", subaccount=1), -]) +result = client.orders.batch_cancel_v2(request=BatchCancelOrdersV2Request( + orders=[ + BatchCancelOrdersV2RequestOrder(order_id="ord_abc", subaccount=0), + BatchCancelOrdersV2RequestOrder(order_id="ord_def", subaccount=1), + ], +)) +for entry in result.orders: + if entry.error is not None: + print("failed:", entry.order_id, entry.error) + else: + print(entry.order_id, "canceled") ``` -!!! note "v0.8.0 wire change" - The body field is `orders` (an array of objects). The previous `ids` - field is no longer emitted. If you talk to the API directly elsewhere, - keep that in mind. - !!! info "Cancel is not server-idempotent" - `cancel(order_id)` (and `cancel_v2(...)`, `batch_cancel(...)`) propagate - a 404 as `KalshiNotFoundError` when the order is already canceled, fully - filled, or never existed. The SDK does **not** automatically swallow - that — the caller owns safe-retry idempotency. Treat 404 as "already - canceled" only if you can rule out a typo'd `order_id`: + `cancel_v2(...)` (and `batch_cancel_v2(...)`) propagate a 404 as + `KalshiNotFoundError` when the order is already canceled, fully filled, or + never existed. The SDK does **not** automatically swallow that — the + caller owns safe-retry idempotency. Treat 404 as "already canceled" only + if you can rule out a typo'd `order_id`: ```python from kalshi.errors import KalshiNotFoundError try: - client.orders.cancel(order_id) + client.orders.cancel_v2(order_id) except KalshiNotFoundError: pass # already canceled — idempotent ``` @@ -170,20 +159,25 @@ result = client.orders.batch_cancel([ ## Amend ```python -resp = client.orders.amend( +from decimal import Decimal +from kalshi import AmendOrderV2Request + +resp = client.orders.amend_v2( "ord_abc", - ticker="KXPRES-24-DJT", # required — same shape as create - side="yes", # required - action="buy", # required - yes_price="0.66", # at least one of yes_price / no_price / count - updated_client_order_id="cid-2", + subaccount=0, # query param + request=AmendOrderV2Request( + ticker="KXPRES-24-DJT", # same shape as create + side="bid", # BookSideLiteral + price=Decimal("0.66"), + count=Decimal("12"), # total/max fillable count + exchange_index=0, # body field + ), ) print(resp.old_order.order_id, resp.order.order_id) ``` -`AmendOrderResponse` carries both the previous and the new order. `ticker`, -`side`, and `action` are required even though you're amending an existing -order — they're part of the API's amend payload. +`AmendOrderV2Response` carries both the previous and the new order. `ticker` +and `side` are part of the API's amend payload. ## Decrease @@ -191,12 +185,23 @@ Reduce the size of a resting order without canceling. Exactly one of `reduce_by` or `reduce_to`: ```python -order = client.orders.decrease("ord_abc", reduce_by=3) # decrease size by 3 -order = client.orders.decrease("ord_abc", reduce_to=5) # decrease size to 5 +from decimal import Decimal +from kalshi import DecreaseOrderV2Request + +resp = client.orders.decrease_v2( + "ord_abc", + subaccount=0, + request=DecreaseOrderV2Request(reduce_by=Decimal("3")), # decrease size by 3 +) + +resp = client.orders.decrease_v2( + "ord_abc", + request=DecreaseOrderV2Request(reduce_to=Decimal("5")), # decrease size to 5 +) ``` -Passing neither or both raises `ValidationError` at construction (XOR enforced -by the model). +`DecreaseOrderV2Request` enforces XOR on `reduce_by` / `reduce_to` at +construction — passing both or neither raises `ValidationError`. ## List orders @@ -226,124 +231,14 @@ q = client.orders.queue_position("ord_abc") # returns Decimal `queue_position(order_id)` returns a bare `Decimal`, not a model — that's the shape Kalshi's endpoint emits. -## V2 event-market orders - -Spec v3.18.0 introduced the `/portfolio/events/orders/*` family. Legacy -`/portfolio/orders` keeps working and will be deprecated **no earlier than -May 6, 2026** — start new event-market integrations on V2. - -### How V2 differs from V1 - -| | V1 (`/portfolio/orders`) | V2 (`/portfolio/events/orders`) | -|---|---|---| -| Side enum | `SideLiteral` — `"yes"` / `"no"` | `BookSideLiteral` — `"bid"` / `"ask"` | -| Price | Paired `yes_price` / `no_price` | Single `price: DollarDecimal` | -| `client_order_id` | Optional | **Required** + acts as server-side idempotency key | -| Kwarg API | Yes (kwargs or request model) | Model-only — pass a request model | -| Side filter | n/a | per-spec query/body asymmetry — see [Asymmetry](#asymmetry) | - -### Place a V2 order - -```python -import uuid -from decimal import Decimal -from kalshi import KalshiClient, CreateOrderV2Request - -with KalshiClient.from_env() as client: - resp = client.orders.create_v2(request=CreateOrderV2Request( - ticker="EVENT-MKT-A", - client_order_id=str(uuid.uuid4()), # required — idempotency key - side="bid", # BookSideLiteral - count=Decimal("10"), - price=Decimal("0.50"), - time_in_force="good_till_canceled", - self_trade_prevention_type="taker_at_cross", - # post_only=False, reduce_only=False, cancel_order_on_pause=False, - # subaccount=0, order_group_id="og_abc", exchange_index=0, - )) - print(resp.order_id, resp.remaining_count, resp.fill_count) -``` - -Reusing a `client_order_id` for a second call returns the **original** -order rather than placing a new one. Generate a fresh UUID4 per request. - -### Cancel V2 - -```python -result = client.orders.cancel_v2("ord_v2_1", subaccount=0, exchange_index=0) -print(result.reduced_by, result.ts_ms) # FixedPointCount, int -``` - -`cancel_v2` returns a `CancelOrderV2Response` (with the count actually -canceled), not `None` like the V1 `cancel`. A 204 No Content from the -server is treated as a protocol violation and raises `KalshiError`. - -### Amend V2 / Decrease V2 - -```python -from decimal import Decimal -from kalshi import AmendOrderV2Request, DecreaseOrderV2Request - -resp = client.orders.amend_v2( - "ord_v2_1", - subaccount=0, # query param - request=AmendOrderV2Request( - ticker="EVENT-MKT-A", - side="bid", - price=Decimal("0.55"), - count=Decimal("12"), # total/max fillable count - exchange_index=0, # body field - ), -) - -resp = client.orders.decrease_v2( - "ord_v2_1", - subaccount=0, - request=DecreaseOrderV2Request( - reduce_by=Decimal("3"), # exactly one of reduce_by | reduce_to - ), -) -``` - -`DecreaseOrderV2Request` enforces XOR on `reduce_by` / `reduce_to` at -construction — passing both or neither raises `ValidationError`. - -### Batch V2 - -```python -from kalshi import ( - BatchCreateOrdersV2Request, BatchCancelOrdersV2Request, - BatchCancelOrdersV2RequestOrder, -) - -# Create -result = client.orders.batch_create_v2(request=BatchCreateOrdersV2Request( - orders=[ - CreateOrderV2Request(...), # per-order builds, same shape as create_v2 - CreateOrderV2Request(...), - ], -)) -for entry in result.orders: - if entry.error is not None: - print("failed:", entry.error) - else: - print(entry.order_id, entry.fill_count) - -# Cancel -result = client.orders.batch_cancel_v2(request=BatchCancelOrdersV2Request( - orders=[ - BatchCancelOrdersV2RequestOrder(order_id="ord_a", subaccount=0), - BatchCancelOrdersV2RequestOrder(order_id="ord_b"), - ], -)) -``` +## Batch error handling Per-entry error handling is built into `BatchCreateOrdersV2ResponseEntry` and `BatchCancelOrdersV2ResponseEntry` — successful entries carry the order/fill data, failed entries carry an `error` dict with the rest of the fields nulled (create) or zeroed (cancel). -### Asymmetry { #asymmetry } +## `subaccount` / `exchange_index` routing { #asymmetry } This trips people up. The V2 spec routes `subaccount` and `exchange_index` to **different places** depending on the endpoint: @@ -355,7 +250,7 @@ to **different places** depending on the endpoint: | `decrease_v2` | query | **body** (on `DecreaseOrderV2Request`) | | `create_v2` / `batch_*_v2` | n/a — body on request model | body | -This is faithful to OpenAPI v3.18.0 — `amend_v2`/`decrease_v2` only +This is faithful to the OpenAPI spec — `amend_v2`/`decrease_v2` only declare `SubaccountQueryDefaultPrimary` in their `parameters` list, while `cancel_v2` declares both that and `ExchangeIndexQuery`. Don't try to "normalize" it; the SDK matches the spec. diff --git a/docs/retries.md b/docs/retries.md index a4516b2..7e9e6f6 100644 --- a/docs/retries.md +++ b/docs/retries.md @@ -95,10 +95,11 @@ explicitly. POST/DELETE/PUT not retrying means **idempotency is your responsibility** on write paths. The patterns that work: -### `client_order_id` on `orders.create` / `orders.amend` +### `client_order_id` on `orders.create_v2` / `orders.amend_v2` -Set a unique `client_order_id` (a UUID is fine). On a network failure between -"server processed the order" and "you got the response": +`client_order_id` is **required** on `CreateOrderV2Request` (a UUID is fine) — +the server uses it for idempotency. On a network failure between "server +processed the order" and "you got the response": 1. Catch the exception. 2. Call `client.orders.list(min_ts=..., max_ts=...)` and look for the @@ -107,15 +108,18 @@ Set a unique `client_order_id` (a UUID is fine). On a network failure between ```python import uuid -from kalshi import KalshiClient, KalshiError +from decimal import Decimal +from kalshi import KalshiClient, KalshiError, CreateOrderV2Request with KalshiClient.from_env() as client: cid = str(uuid.uuid4()) + request = CreateOrderV2Request( + ticker="X", client_order_id=cid, side="bid", + count=Decimal("10"), price=Decimal("0.65"), + time_in_force="fill_or_kill", self_trade_prevention_type="taker_at_cross", + ) try: - order = client.orders.create( - ticker="X", side="yes", action="buy", count=10, - yes_price="0.65", client_order_id=cid, - ) + order = client.orders.create_v2(request=request) except KalshiError: # Did it land? Find out before retrying. existing = next( @@ -123,10 +127,7 @@ with KalshiClient.from_env() as client: None, ) if existing is None: - order = client.orders.create( - ticker="X", side="yes", action="buy", count=10, - yes_price="0.65", client_order_id=cid, - ) + order = client.orders.create_v2(request=request) else: order = existing ``` @@ -138,9 +139,9 @@ Reconcile via `subaccounts.list_transfers()`. ### Cancels -`client.orders.cancel(order_id)` and `client.orders.batch_cancel(...)` are -already idempotent server-side: re-canceling a canceled order is a no-op. You -can safely retry these from your app layer without dedupe logic. +`client.orders.cancel_v2(order_id)` and `client.orders.batch_cancel_v2(...)` +are already idempotent server-side: re-canceling a canceled order is a no-op. +You can safely retry these from your app layer without dedupe logic. ### Reads @@ -148,8 +149,8 @@ Just call them again. The transport's built-in retry covers transient 5xx. ## Reconciliation after a 5xx on a write -`KalshiServerError` from `POST /portfolio/orders` is ambiguous — the order -might have made it into the book before the server gave up. The recovery: +`KalshiServerError` from `POST /portfolio/events/orders` is ambiguous — the +order might have made it into the book before the server gave up. The recovery: 1. Wait briefly (a few seconds — orders propagate fast). 2. List recent orders matching your `client_order_id`. diff --git a/docs/types.md b/docs/types.md index 29afa52..e729cab 100644 --- a/docs/types.md +++ b/docs/types.md @@ -19,10 +19,26 @@ Used for every price field (`yes_bid`, `yes_ask`, `no_bid`, `no_ask`, ```python from decimal import Decimal -from kalshi import CreateOrderRequest - -CreateOrderRequest(ticker="X", side="yes", action="buy", count=1, yes_price="0.65") -CreateOrderRequest(ticker="X", side="yes", action="buy", count=1, yes_price=Decimal("0.65")) +from kalshi import CreateOrderV2Request + +CreateOrderV2Request( + ticker="X", + client_order_id="my-id-1", + side="bid", + count=Decimal(1), + price="0.65", + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", +) +CreateOrderV2Request( + ticker="X", + client_order_id="my-id-2", + side="bid", + count=Decimal(1), + price=Decimal("0.65"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", +) ``` The Kalshi API uses **FixedPointDollars** strings (`"0.5600"`) with up to six @@ -39,8 +55,8 @@ directly. ### `OrderPrice` -Request-side dollar price fields (`CreateOrderRequest.yes_price` / `no_price`, -`AmendOrderRequest`, and the V2 order equivalents) use `OrderPrice` — the same +Request-side dollar price fields (`CreateOrderV2Request.price`, +`AmendOrderV2Request.price`) use `OrderPrice` — the same wire form and coercion as `DollarDecimal`, plus two construction-time guards: the price must be **non-negative** and aligned to the **$0.0001 tick**. A negative or sub-tick value (`Decimal("-0.65")`, `Decimal("0.12345")`) raises `ValueError` at @@ -64,7 +80,6 @@ Some fields are plain `int` cents, **not** `DollarDecimal`: | `Balance.balance` / `portfolio_value` | `client.portfolio.balance()` | cents | | `Deposit.amount_cents` / `fee_cents` | `client.portfolio.deposits()` | cents | | `Withdrawal.amount_cents` / `fee_cents` | `client.portfolio.withdrawals()` | cents | -| `CreateOrderRequest.buy_max_cost` | `client.orders.create(..., buy_max_cost=500)` | cents — `500` means $5.00 | | `ApplySubaccountTransferRequest.amount_cents` | `client.subaccounts.transfer(...)` | cents | Passing a `Decimal` or `float` to one of these raises `ValueError` at @@ -90,9 +105,8 @@ Every enum-like kwarg uses a `Literal` alias so your IDE auto-completes and | Alias | Values | |---|---| -| `SideLiteral` | `"yes"`, `"no"` (V1 orders) | +| `SideLiteral` | `"yes"`, `"no"` (outcome side on reads — e.g. `Order.outcome_side`, `Fill.outcome_side`) | | `BookSideLiteral` | `"bid"`, `"ask"` (V2 event-market orders) | -| `ActionLiteral` | `"buy"`, `"sell"` | | `TimeInForceLiteral` | `"fill_or_kill"`, `"good_till_canceled"`, `"immediate_or_cancel"` | | `SelfTradePreventionTypeLiteral` | `"taker_at_cross"`, `"maker"` | | `OrderStatusLiteral` | `"resting"`, `"canceled"`, `"executed"` | @@ -115,9 +129,13 @@ Every enum-like kwarg uses a `Literal` alias so your IDE auto-completes and All aliases are exported from the top-level `kalshi` package: ```python -from kalshi import SideLiteral, ActionLiteral, TimeInForceLiteral +from kalshi import BookSideLiteral, TimeInForceLiteral, SelfTradePreventionTypeLiteral -def place(side: SideLiteral, action: ActionLiteral, tif: TimeInForceLiteral) -> None: +def place( + side: BookSideLiteral, + tif: TimeInForceLiteral, + stp: SelfTradePreventionTypeLiteral, +) -> None: ... ``` diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 16895a9..f24ee85 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -33,9 +33,6 @@ AccountApiUsageLevelVolumeProgress, AccountEndpointCosts, AccountVolumeProgress, - ActionLiteral, - AmendOrderRequest, - AmendOrderResponse, AmendOrderV2Request, AmendOrderV2Response, Announcement, @@ -44,17 +41,10 @@ ApplySubaccountTransferRequest, AssociatedEvent, Balance, - BatchCancelOrdersRequest, - BatchCancelOrdersRequestOrder, - BatchCancelOrdersResponse, - BatchCancelOrdersResponseEntry, BatchCancelOrdersV2Request, BatchCancelOrdersV2RequestOrder, BatchCancelOrdersV2Response, BatchCancelOrdersV2ResponseEntry, - BatchCreateOrdersRequest, - BatchCreateOrdersResponse, - BatchCreateOrdersResponseEntry, BatchCreateOrdersV2Request, BatchCreateOrdersV2Response, BatchCreateOrdersV2ResponseEntry, @@ -69,7 +59,6 @@ CreateMarketResponse, CreateOrderGroupRequest, CreateOrderGroupResponse, - CreateOrderRequest, CreateOrderV2Request, CreateOrderV2Response, CreateQuoteRequest, @@ -78,7 +67,6 @@ CreateRFQResponse, CreateSubaccountResponse, DailySchedule, - DecreaseOrderRequest, DecreaseOrderV2Request, DecreaseOrderV2Response, Deposit, @@ -207,9 +195,6 @@ "AccountApiUsageLevelVolumeProgress", "AccountEndpointCosts", "AccountVolumeProgress", - "ActionLiteral", - "AmendOrderRequest", - "AmendOrderResponse", "AmendOrderV2Request", "AmendOrderV2Response", "Announcement", @@ -225,17 +210,10 @@ "AsyncRFQsResource", "AuthRequiredError", "Balance", - "BatchCancelOrdersRequest", - "BatchCancelOrdersRequestOrder", - "BatchCancelOrdersResponse", - "BatchCancelOrdersResponseEntry", "BatchCancelOrdersV2Request", "BatchCancelOrdersV2RequestOrder", "BatchCancelOrdersV2Response", "BatchCancelOrdersV2ResponseEntry", - "BatchCreateOrdersRequest", - "BatchCreateOrdersResponse", - "BatchCreateOrdersResponseEntry", "BatchCreateOrdersV2Request", "BatchCreateOrdersV2Response", "BatchCreateOrdersV2ResponseEntry", @@ -251,7 +229,6 @@ "CreateMarketResponse", "CreateOrderGroupRequest", "CreateOrderGroupResponse", - "CreateOrderRequest", "CreateOrderV2Request", "CreateOrderV2Response", "CreateQuoteRequest", @@ -260,7 +237,6 @@ "CreateRFQResponse", "CreateSubaccountResponse", "DailySchedule", - "DecreaseOrderRequest", "DecreaseOrderV2Request", "DecreaseOrderV2Response", "Deposit", @@ -397,4 +373,4 @@ "Withdrawal", ] -__version__ = "4.2.0" +__version__ = "5.0.0" diff --git a/kalshi/_contract_map.py b/kalshi/_contract_map.py index 38c97f9..43c60d3 100644 --- a/kalshi/_contract_map.py +++ b/kalshi/_contract_map.py @@ -38,16 +38,6 @@ class ContractEntry: sdk_model="kalshi.models.orders.Fill", spec_schema="Fill", ), - ContractEntry( - sdk_model="kalshi.models.orders.CreateOrderRequest", - spec_schema="CreateOrderRequest", - notes="Uses serialization_alias (outbound), not validation_alias (inbound)", - ), - ContractEntry( - sdk_model="kalshi.models.orders.AmendOrderResponse", - spec_schema="AmendOrderResponse", - notes="Response wrapper with old_order + order, both are Order instances", - ), ContractEntry( sdk_model="kalshi.models.orders.OrderQueuePosition", spec_schema="OrderQueuePosition", @@ -344,29 +334,6 @@ class ContractEntry: spec_schema="GetMarketOrderbooksResponse", ), # -- orders V2 family (#171) ----------------------------------------- - ContractEntry( - sdk_model="kalshi.models.orders.AmendOrderRequest", - spec_schema="AmendOrderRequest", - notes="Request body for /portfolio/orders/{order_id}/amend", - ), - ContractEntry( - sdk_model="kalshi.models.orders.DecreaseOrderRequest", - spec_schema="DecreaseOrderRequest", - notes="Request body for /portfolio/orders/{order_id}/decrease", - ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCreateOrdersRequest", - spec_schema="BatchCreateOrdersRequest", - ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCancelOrdersRequest", - spec_schema="BatchCancelOrdersRequest", - ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCancelOrdersRequestOrder", - spec_schema="BatchCancelOrdersRequest.orders.items", - notes="Inline object schema (no named component)", - ), ContractEntry( sdk_model="kalshi.models.orders.CreateOrderV2Request", spec_schema="CreateOrderV2Request", @@ -426,24 +393,6 @@ class ContractEntry: spec_schema="BatchCancelOrdersV2Response.orders.items", notes="Inline object schema (no named component)", ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCreateOrdersResponse", - spec_schema="BatchCreateOrdersResponse", - ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCreateOrdersResponseEntry", - spec_schema="BatchCreateOrdersResponse.orders.items", - notes="Inline object schema (no named component)", - ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCancelOrdersResponse", - spec_schema="BatchCancelOrdersResponse", - ), - ContractEntry( - sdk_model="kalshi.models.orders.BatchCancelOrdersResponseEntry", - spec_schema="BatchCancelOrdersResponse.orders.items", - notes="Inline object schema (no named component)", - ), # -- events sub-models (#171) ---------------------------------------- ContractEntry( sdk_model="kalshi.models.events.MarketMetadata", @@ -865,6 +814,10 @@ class ContractEntry: sdk_model="kalshi.perps.klear.models.margin.GetActiveMarginObligationResponse", spec_schema="GetActiveMarginObligationResponse", ), + ContractEntry( + sdk_model="kalshi.perps.klear.models.margin.MarketSettlementEstimate", + spec_schema="MarketSettlementEstimate", + ), ContractEntry( sdk_model="kalshi.perps.klear.models.margin.SettlementEstimate", spec_schema="SettlementEstimate", diff --git a/kalshi/models/__init__.py b/kalshi/models/__init__.py index 9370281..48a5cfb 100644 --- a/kalshi/models/__init__.py +++ b/kalshi/models/__init__.py @@ -109,31 +109,19 @@ UpdateOrderGroupLimitRequest, ) from kalshi.models.orders import ( - ActionLiteral, - AmendOrderRequest, - AmendOrderResponse, AmendOrderV2Request, AmendOrderV2Response, - BatchCancelOrdersRequest, - BatchCancelOrdersRequestOrder, - BatchCancelOrdersResponse, - BatchCancelOrdersResponseEntry, BatchCancelOrdersV2Request, BatchCancelOrdersV2RequestOrder, BatchCancelOrdersV2Response, BatchCancelOrdersV2ResponseEntry, - BatchCreateOrdersRequest, - BatchCreateOrdersResponse, - BatchCreateOrdersResponseEntry, BatchCreateOrdersV2Request, BatchCreateOrdersV2Response, BatchCreateOrdersV2ResponseEntry, BookSideLiteral, CancelOrderV2Response, - CreateOrderRequest, CreateOrderV2Request, CreateOrderV2Response, - DecreaseOrderRequest, DecreaseOrderV2Request, DecreaseOrderV2Response, Fill, @@ -196,9 +184,6 @@ "AccountApiUsageLevelVolumeProgress", "AccountEndpointCosts", "AccountVolumeProgress", - "ActionLiteral", - "AmendOrderRequest", - "AmendOrderResponse", "AmendOrderV2Request", "AmendOrderV2Response", "Announcement", @@ -207,17 +192,10 @@ "ApplySubaccountTransferRequest", "AssociatedEvent", "Balance", - "BatchCancelOrdersRequest", - "BatchCancelOrdersRequestOrder", - "BatchCancelOrdersResponse", - "BatchCancelOrdersResponseEntry", "BatchCancelOrdersV2Request", "BatchCancelOrdersV2RequestOrder", "BatchCancelOrdersV2Response", "BatchCancelOrdersV2ResponseEntry", - "BatchCreateOrdersRequest", - "BatchCreateOrdersResponse", - "BatchCreateOrdersResponseEntry", "BatchCreateOrdersV2Request", "BatchCreateOrdersV2Response", "BatchCreateOrdersV2ResponseEntry", @@ -232,7 +210,6 @@ "CreateMarketResponse", "CreateOrderGroupRequest", "CreateOrderGroupResponse", - "CreateOrderRequest", "CreateOrderV2Request", "CreateOrderV2Response", "CreateQuoteRequest", @@ -241,7 +218,6 @@ "CreateRFQResponse", "CreateSubaccountResponse", "DailySchedule", - "DecreaseOrderRequest", "DecreaseOrderV2Request", "DecreaseOrderV2Response", "Deposit", diff --git a/kalshi/models/orders.py b/kalshi/models/orders.py index 188ddad..d0bd713 100644 --- a/kalshi/models/orders.py +++ b/kalshi/models/orders.py @@ -2,27 +2,20 @@ from __future__ import annotations -from decimal import Decimal from typing import Literal -from pydantic import AliasChoices, AwareDatetime, BaseModel, Field, field_validator, model_validator +from pydantic import AliasChoices, AwareDatetime, BaseModel, Field, model_validator from kalshi.types import DollarDecimal, FixedPointCount, OrderPrice, StrictInt -# Literal aliases for fixed-enum kwargs on order resource methods and the -# matching V1 / V2 request models. -# Source of truth: OpenAPI spec v3.13.0 (specs/openapi.yaml). -# V1 ``CreateOrderRequest`` (#270) and ``AmendOrderRequest`` (#312) plus V2 -# ``CreateOrderV2Request`` all carry these narrowed types so a typo fails -# at construction rather than as a 400 from the server. +# Literal aliases for fixed-enum fields on the order models. +# Source of truth: OpenAPI spec (specs/openapi.yaml). Narrowed types make a +# typo fail at construction rather than as a 400 from the server. SideLiteral = Literal["yes", "no"] -"""Order side. Spec: CreateOrderRequest.side / AmendOrderRequest.side enum.""" - -ActionLiteral = Literal["buy", "sell"] -"""Order action. Spec: CreateOrderRequest.action / AmendOrderRequest.action enum.""" +"""Outcome side. Spec: Order.outcome_side / Fill.outcome_side enum.""" TimeInForceLiteral = Literal["fill_or_kill", "good_till_canceled", "immediate_or_cancel"] -"""Order time-in-force. Spec: CreateOrderRequest.time_in_force enum.""" +"""Order time-in-force. Spec: CreateOrderV2Request.time_in_force enum.""" SelfTradePreventionTypeLiteral = Literal["taker_at_cross", "maker"] """Self-trade prevention behavior. Spec: SelfTradePreventionType enum.""" @@ -144,298 +137,6 @@ class Fill(BaseModel): model_config = {"extra": "allow", "populate_by_name": True} -class CreateOrderRequest(BaseModel): - """Parameters for creating an order. - - Price fields serialize with ``_dollars`` suffix. ``count`` is a Decimal - and serializes as ``count_fp`` (FixedPointCount string); the spec - accepts either ``count`` or ``count_fp`` key, but the SDK commits to - a single wire shape. - - ``buy_max_cost`` is **integer cents** (per OpenAPI spec: "Maximum - cost in cents"). Pass e.g. ``500`` for a $5.00 cap, NOT ``5.00``. - Passing a decimal string like ``"5.00"`` raises ``ValidationError``. - - The SDK previously exposed a ``type: str = "limit"`` field never - defined in the spec's ``CreateOrderRequest`` schema. v0.8.0 removes - it. Callers passing ``type="market"`` (or similar) now get a - ``ValidationError`` at construction time. - - ``ticker``, ``side``, ``action``, and ``count`` are all required by the - spec. Pre-v2.3.0 the SDK defaulted ``action`` to ``"buy"`` (#172); the - follow-up (#242) also removed the silent ``count=1`` default — both are - money-risk drivers, since a missing arg would otherwise translate into - a real 1-contract BUY rather than a clear error. - - See ``kalshi.resources.orders.OrdersResource.create`` for the - user-facing method that builds this model internally. - """ - - ticker: str - side: SideLiteral - action: ActionLiteral - count: FixedPointCount = Field(serialization_alias="count_fp") - yes_price: OrderPrice | None = Field( - default=None, - serialization_alias="yes_price_dollars", - ) - no_price: OrderPrice | None = Field( - default=None, - serialization_alias="no_price_dollars", - ) - client_order_id: str | None = None - expiration_ts: StrictInt | None = None - buy_max_cost: int | None = None - time_in_force: TimeInForceLiteral | None = None - post_only: bool | None = None - reduce_only: bool | None = None - self_trade_prevention_type: SelfTradePreventionTypeLiteral | None = None - order_group_id: str | None = None - cancel_order_on_pause: bool | None = None - subaccount: StrictInt | None = Field(default=None, ge=0) - exchange_index: StrictInt | None = Field(default=None, ge=0) - - model_config = {"extra": "forbid"} - - @field_validator("buy_max_cost", mode="before") - @classmethod - def _reject_decimal_float_and_bool_buy_max_cost(cls, v: object) -> object: - """Reject Decimal, float, and bool inputs on buy_max_cost. - - Spec says integer cents. Accepting Decimal would silently coerce - callers who pass Decimal('5.00') (expecting $5.00 under the old - DollarDecimal semantics) into 5 cents — data corruption with no - error. Bool is an ``int`` subclass, so ``True`` would otherwise - slip through as 1 (= 1 cent cap), the same class of bug #225 - closed for ``DollarDecimal`` / ``FixedPointCount``. Reject at - the boundary (#243). - - int and int-shaped strings are fine (Pydantic coerces normally). - """ - if isinstance(v, bool): - raise ValueError( - "buy_max_cost must be int (cents), not bool — " - "bool is an int subclass, so True would otherwise slip " - "through as 1 (= 1 cent cap). Pass cents directly " - "(e.g., 500 for $5.00)." - ) - if isinstance(v, Decimal): - raise ValueError( - "buy_max_cost must be int (cents), not Decimal. " - "The previous DollarDecimal type was a v0.7.x-and-earlier " - "bug — spec says integer cents. Pass cents directly " - "(e.g., 500 for $5.00)." - ) - if isinstance(v, float): - raise ValueError( - "buy_max_cost must be int (cents), not float. " - "Pass cents directly (e.g., 500 for $5.00)." - ) - return v - - -class AmendOrderRequest(BaseModel): - """Parameters for amending an open order. - - Matches spec ``components.schemas.AmendOrderRequest``. Required fields - (``ticker``, ``side``, ``action``) mirror the spec's ``required`` list. - Price fields serialize with ``_dollars`` suffix; ``count`` serializes - as ``count_fp`` (FixedPointCount). - - Cent-form ``yes_price``/``no_price`` spec properties are NOT on this - model — redundant with the ``_dollars`` forms. EXCLUSIONS in - ``tests/_contract_support.py`` records this. - - See ``kalshi.resources.orders.OrdersResource.amend`` — v0.8.0 builds - this model internally; the public method signature is unchanged. - """ - - ticker: str - side: SideLiteral - action: ActionLiteral - yes_price: OrderPrice | None = Field( - default=None, - serialization_alias="yes_price_dollars", - ) - no_price: OrderPrice | None = Field( - default=None, - serialization_alias="no_price_dollars", - ) - count: FixedPointCount | None = Field( - default=None, - serialization_alias="count_fp", - ) - client_order_id: str | None = None - updated_client_order_id: str | None = None - subaccount: StrictInt | None = Field(default=None, ge=0) - exchange_index: StrictInt | None = Field(default=None, ge=0) - - model_config = {"extra": "forbid"} - - -class DecreaseOrderRequest(BaseModel): - """Parameters for decreasing an open order's size. - - Matches spec ``components.schemas.DecreaseOrderRequest``. Spec marks - all fields optional, but the server rejects an empty body — so the - model enforces XOR at construction: exactly one of ``reduce_by`` or - ``reduce_to`` must be set. This matches the method-level guard in - ``orders.decrease()`` and keeps model-first construction (v0.9) - fail-fast rather than deferring the error to the HTTP call. - - See ``kalshi.resources.orders.OrdersResource.decrease`` — v0.8.0 - builds this model internally; method signature unchanged. - """ - - reduce_by: StrictInt | None = None - reduce_to: StrictInt | None = None - subaccount: StrictInt | None = Field(default=None, ge=0) - exchange_index: StrictInt | None = Field(default=None, ge=0) - - model_config = {"extra": "forbid"} - - @model_validator(mode="after") - def _enforce_reduce_xor(self) -> DecreaseOrderRequest: - if self.reduce_by is not None and self.reduce_to is not None: - raise ValueError( - "DecreaseOrderRequest accepts reduce_by or reduce_to, not both" - ) - if self.reduce_by is None and self.reduce_to is None: - raise ValueError( - "DecreaseOrderRequest requires either reduce_by or reduce_to" - ) - return self - - -class BatchCreateOrdersRequest(BaseModel): - """Wrapper for the ``POST /portfolio/orders/batched`` request body. - - Matches spec ``components.schemas.BatchCreateOrdersRequest``: a single - ``orders`` key holding a list of ``CreateOrderRequest`` entries. Each - nested entry inherits ``extra="forbid"`` from ``CreateOrderRequest`` - itself, so phantom fields in items fail at construction time. - - See ``kalshi.resources.orders.OrdersResource.batch_create`` — v0.8.0 - wraps this model internally; method signature unchanged. - """ - - orders: list[CreateOrderRequest] - - model_config = {"extra": "forbid"} - - -class BatchCancelOrdersRequestOrder(BaseModel): - """A single cancellation entry in a batch cancel request. - - Matches spec ``components.schemas.BatchCancelOrdersRequestOrder``. - Required: ``order_id``. Optional: ``subaccount`` (defaults to 0, - primary subaccount). - """ - - order_id: str - subaccount: StrictInt | None = Field(default=None, ge=0) - exchange_index: StrictInt | None = Field(default=None, ge=0) - - model_config = {"extra": "forbid"} - - -class BatchCancelOrdersRequest(BaseModel): - """Wrapper for the ``DELETE /portfolio/orders/batched`` request body. - - Matches spec ``components.schemas.BatchCancelOrdersRequest``. Spec - defines two fields: the preferred ``orders`` (list of - ``BatchCancelOrdersRequestOrder``) and the deprecated ``ids`` (list - of string order IDs). SDK v0.8.0 commits to emitting ``orders`` only. - - The previous SDK sent the deprecated ``ids`` field — BREAKING change - at the wire level as of v0.8.0. Users calling the public - ``batch_cancel(orders=[...])`` method are unaffected. - - See ``kalshi.resources.orders.OrdersResource.batch_cancel``. - """ - - orders: list[BatchCancelOrdersRequestOrder] - - model_config = {"extra": "forbid"} - - -class BatchCreateOrdersResponseEntry(BaseModel): - """Single entry in :class:`BatchCreateOrdersResponse`. - - Spec ``components.schemas.BatchCreateOrdersIndividualResponse``: all - three fields are nullable. A failed leg comes back as - ``{"client_order_id": "x", "order": null, "error": {...}}``; a - successful leg as ``{"client_order_id": "x", "order": {...}, "error": null}``. - Pairing returned orders with the originating request requires - ``client_order_id``; surfacing per-leg failures requires ``error``. - """ - - order: Order | None = None - error: dict[str, object] | None = None - client_order_id: str | None = None - - model_config = {"extra": "allow", "populate_by_name": True} - - -class BatchCreateOrdersResponse(BaseModel): - """Response from ``POST /portfolio/orders/batched``. - - Spec ``components.schemas.BatchCreateOrdersResponse``. Changed in v2.4.0 - (breaking): previously the SDK returned ``list[Order]`` and crashed - with ``ValidationError`` on the first failed leg - (``Order.model_validate(None)``). Now returns the typed envelope so - callers can inspect per-leg ``order``/``error``/``client_order_id``. - """ - - orders: list[BatchCreateOrdersResponseEntry] - - model_config = {"extra": "allow", "populate_by_name": True} - - -class BatchCancelOrdersResponseEntry(BaseModel): - """Single entry in :class:`BatchCancelOrdersResponse`. - - Spec ``components.schemas.BatchCancelOrdersIndividualResponse``: - ``order_id`` and ``reduced_by_fp`` are required; ``order`` and - ``error`` are nullable. ``reduced_by_fp`` is load-bearing for risk - reconciliation — it is the count of contracts that actually canceled - (``0`` when ``error`` is set, the canceled count otherwise). - """ - - order_id: str - reduced_by_fp: FixedPointCount = Field( - validation_alias=AliasChoices("reduced_by_fp", "reduced_by"), - ) - order: Order | None = None - error: dict[str, object] | None = None - - model_config = {"extra": "allow", "populate_by_name": True} - - -class BatchCancelOrdersResponse(BaseModel): - """Response from ``DELETE /portfolio/orders/batched``. - - Spec ``components.schemas.BatchCancelOrdersResponse``. Changed in v2.4.0 - (breaking): previously the SDK declared ``-> None`` and discarded the - response body. Per-leg ``reduced_by_fp`` and any per-leg errors are - now surfaced. - """ - - orders: list[BatchCancelOrdersResponseEntry] - - model_config = {"extra": "allow", "populate_by_name": True} - - - -class AmendOrderResponse(BaseModel): - """Response from amending an order — contains both pre and post-amendment orders.""" - - old_order: Order - order: Order - - model_config = {"extra": "allow"} - - class OrderQueuePosition(BaseModel): """Queue position for a single resting order.""" @@ -456,19 +157,18 @@ class OrderQueuePosition(BaseModel): class CreateOrderV2Request(BaseModel): - """Body for POST /portfolio/events/orders. - - Differences from v1 ``CreateOrderRequest`` worth knowing: - - - ``side`` is ``BookSideLiteral`` (``bid``/``ask``), not ``yes``/``no``. - V2 narrows the type on the model itself since there is no kwarg - overload at the resource-method boundary (see model_only V2 surface). - - ``client_order_id`` is **required** by this model, unlike V1 where it is - optional, because the server uses it for idempotency on V2 orders. - (OpenAPI v3.20.0 relaxed it to optional server-side, but the SDK keeps - it required by design so every V2 order is idempotent.) - - Price is a single ``price: DollarDecimal`` field rather than the - paired ``yes_price`` / ``no_price`` from V1. + """Body for POST /portfolio/events/orders — the event-market order API. + + Notable shape: + + - ``side`` is ``BookSideLiteral`` (``bid``/``ask``). V2 narrows the type on + the model itself since there is no kwarg overload at the resource-method + boundary (the V2 surface is model-only). + - ``client_order_id`` is **required** by this model because the server uses + it for idempotency on V2 orders. (OpenAPI v3.20.0 relaxed it to optional + server-side, but the SDK keeps it required by design so every V2 order is + idempotent.) + - Price is a single ``price`` field (dollars). """ ticker: str @@ -626,7 +326,12 @@ class BatchCancelOrdersV2RequestOrder(BaseModel): order_id: str subaccount: StrictInt | None = Field(default=None, ge=0) - exchange_index: StrictInt | None = Field(default=None, ge=0) + # No ``ge=0``: the spec ExchangeIndex permits ``-1`` to auto-route by market + # ticker (mirrors ``cancel_v2``'s plain-int param), in which case + # ``market_ticker`` below is required. + exchange_index: StrictInt | None = None + # Spec v3.22.0: required when exchange_index is -1 (auto-route by ticker). + market_ticker: str | None = None model_config = {"extra": "forbid"} diff --git a/kalshi/models/subaccounts.py b/kalshi/models/subaccounts.py index bb048c0..4a85111 100644 --- a/kalshi/models/subaccounts.py +++ b/kalshi/models/subaccounts.py @@ -20,9 +20,8 @@ class CreateSubaccountResponse(BaseModel): class ApplySubaccountTransferRequest(BaseModel): """Body for POST /portfolio/subaccounts/transfer. - ``amount_cents`` is integer cents per spec (matches the ``buy_max_cost`` - convention on ``CreateOrderRequest``). Pass ``500`` for $5.00, never - a Decimal. ``from_subaccount`` / ``to_subaccount`` use ``0`` for the + ``amount_cents`` is integer cents per spec (e.g. ``500`` for $5.00, never + a Decimal). ``from_subaccount`` / ``to_subaccount`` use ``0`` for the primary account and a positive integer for numbered subaccounts. The server is the source of truth for the upper bound: spec describes ``1-63`` in prose but defines no JSON-schema maximum, and demo has @@ -49,6 +48,8 @@ class SubaccountBalance(BaseModel): """ subaccount_number: int + # Spec v3.22.0: exchange shard the balance is held on (required). + exchange_index: int balance: DollarDecimal updated_ts: int diff --git a/kalshi/perps/__init__.py b/kalshi/perps/__init__.py index 16c8b2c..bb4db86 100644 --- a/kalshi/perps/__init__.py +++ b/kalshi/perps/__init__.py @@ -42,6 +42,7 @@ MaintenanceMarginDetail, MarginReport, MarginReportTypeLiteral, + MarketSettlementEstimate, ObligationEntry, ObligationReceiveInfo, SettlementBalanceHistoryEntry, @@ -296,6 +297,7 @@ "MarginTradePayload", "MarginUserOrderMessage", "MarginUserOrderPayload", + "MarketSettlementEstimate", "MessageQueue", "NotionalRiskLimitResponse", "ObligationEntry", diff --git a/kalshi/perps/klear/models/__init__.py b/kalshi/perps/klear/models/__init__.py index d800ab5..b46831e 100644 --- a/kalshi/perps/klear/models/__init__.py +++ b/kalshi/perps/klear/models/__init__.py @@ -15,6 +15,7 @@ MaintenanceMarginDetail, MarginReport, MarginReportTypeLiteral, + MarketSettlementEstimate, ObligationEntry, ObligationReceiveInfo, SettlementBalanceHistoryEntry, @@ -38,6 +39,7 @@ "MaintenanceMarginDetail", "MarginReport", "MarginReportTypeLiteral", + "MarketSettlementEstimate", "ObligationEntry", "ObligationReceiveInfo", "SettlementBalanceHistoryEntry", diff --git a/kalshi/perps/klear/models/margin.py b/kalshi/perps/klear/models/margin.py index 2fc88e7..7cd2ade 100644 --- a/kalshi/perps/klear/models/margin.py +++ b/kalshi/perps/klear/models/margin.py @@ -206,6 +206,16 @@ def has_next(self) -> bool: model_config = {"extra": "allow"} +class MarketSettlementEstimate(BaseModel): + """Spec ``MarketSettlementEstimate`` — per-market settlement breakdown (centi units).""" + + quantity_centicount: int + variation_margin_centicents: int + notional_value_centicents: int + + model_config = {"extra": "allow"} + + class SettlementEstimate(BaseModel): """Spec ``SettlementEstimate`` — estimated next-settlement amounts (centicents).""" @@ -214,6 +224,8 @@ class SettlementEstimate(BaseModel): maintenance_margin_delta_centicents: int maintenance_margin_required_centicents: int total_amount_centicents: int + # Spec v3.22.0: per-market breakdown map (market ticker -> estimate); optional. + positions: dict[str, MarketSettlementEstimate] | None = None model_config = {"extra": "allow"} @@ -223,10 +235,14 @@ class GetSettlementEstimateResponse(BaseModel): ``subtrader_breakdowns`` is the spec ``additionalProperties`` map (subtrader-id → :class:`SettlementEstimate`); optional. + + ``prev_settlement_prices`` (spec v3.22.0) maps market ticker → most + recent settlement (mark) price in centicents; optional. """ user_breakdown: SettlementEstimate subtrader_breakdowns: dict[str, SettlementEstimate] | None = None + prev_settlement_prices: dict[str, int] | None = None settlement_balance_centicents: int model_config = {"extra": "allow"} diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index c5bf78c..4312179 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -111,8 +111,6 @@ def _list_quotes_params( limit: int | None, min_ts: int | None, max_ts: int | None, - event_ticker: str | None, - market_ticker: str | None, status: QuoteStatusLiteral | None, quote_creator_user_id: str | None, rfq_creator_user_id: str | None, @@ -127,8 +125,6 @@ def _list_quotes_params( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -478,8 +474,6 @@ def list( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -501,8 +495,6 @@ def list( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -521,8 +513,6 @@ def list_all( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -546,8 +536,6 @@ def list_all( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -662,6 +650,83 @@ def confirm(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) extra_headers=extra_headers, ) + # -- RFQ-scoped quote actions (spec v3.22.0) ------------------------------- + # /communications/rfqs/{rfq_id}/quotes/{quote_id}[/accept|/confirm]. + # Same semantics as the flat quote actions above, but scoped to the RFQ. + + def delete_for_rfq( + self, rfq_id: str, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """Delete a quote scoped to its RFQ (spec v3.22.0). + + ``DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id}`` — once + deleted the quote can no longer be accepted. + """ + self._require_auth() + self._delete( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}" + f"/quotes/{_seg(quote_id, name='quote_id')}", + extra_headers=extra_headers, + ) + + @overload + def accept_for_rfq( + self, + rfq_id: str, + quote_id: str, + *, + request: AcceptQuoteRequest, + extra_headers: dict[str, str] | None = None, + ) -> None: ... + @overload + def accept_for_rfq( + self, + rfq_id: str, + quote_id: str, + *, + accepted_side: Literal["yes", "no"], + extra_headers: dict[str, str] | None = None, + ) -> None: ... + def accept_for_rfq( + self, + rfq_id: str, + quote_id: str, + *, + request: AcceptQuoteRequest | None = None, + accepted_side: Literal["yes", "no"] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: + """Accept a quote scoped to its RFQ (spec v3.22.0). + + ``PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept`` — + acceptance still requires the quoter to confirm. + """ + self._require_auth() + body = _build_accept_quote_body(request, accepted_side=accepted_side) + self._put( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}" + f"/quotes/{_seg(quote_id, name='quote_id')}/accept", + json=body, + extra_headers=extra_headers, + ) + + def confirm_for_rfq( + self, rfq_id: str, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """Confirm a quote scoped to its RFQ (spec v3.22.0). + + ``PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm`` — + starts the order-execution timer. + """ + self._require_auth() + # json={} forces Content-Type: application/json — demo rejects empty PUTs. + self._put( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}" + f"/quotes/{_seg(quote_id, name='quote_id')}/confirm", + json={}, + extra_headers=extra_headers, + ) + class BlockTradeProposalsResource(SyncResource): """Sync block-trade-proposals sub-resource. @@ -953,8 +1018,6 @@ def list_quotes( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -970,8 +1033,6 @@ def list_quotes( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -989,8 +1050,6 @@ def list_all_quotes( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -1006,8 +1065,6 @@ def list_all_quotes( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -1216,8 +1273,6 @@ async def list( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -1239,8 +1294,6 @@ async def list( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -1259,8 +1312,6 @@ def list_all( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -1285,8 +1336,6 @@ def list_all( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -1405,6 +1454,78 @@ async def confirm( extra_headers=extra_headers, ) + # -- RFQ-scoped quote actions (spec v3.22.0) ------------------------------- + + async def delete_for_rfq( + self, rfq_id: str, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """Delete a quote scoped to its RFQ (spec v3.22.0). + + ``DELETE /communications/rfqs/{rfq_id}/quotes/{quote_id}``. + """ + self._require_auth() + await self._delete( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}" + f"/quotes/{_seg(quote_id, name='quote_id')}", + extra_headers=extra_headers, + ) + + @overload + async def accept_for_rfq( + self, + rfq_id: str, + quote_id: str, + *, + request: AcceptQuoteRequest, + extra_headers: dict[str, str] | None = None, + ) -> None: ... + @overload + async def accept_for_rfq( + self, + rfq_id: str, + quote_id: str, + *, + accepted_side: Literal["yes", "no"], + extra_headers: dict[str, str] | None = None, + ) -> None: ... + async def accept_for_rfq( + self, + rfq_id: str, + quote_id: str, + *, + request: AcceptQuoteRequest | None = None, + accepted_side: Literal["yes", "no"] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: + """Accept a quote scoped to its RFQ (spec v3.22.0). + + ``PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept``. + """ + self._require_auth() + body = _build_accept_quote_body(request, accepted_side=accepted_side) + await self._put( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}" + f"/quotes/{_seg(quote_id, name='quote_id')}/accept", + json=body, + extra_headers=extra_headers, + ) + + async def confirm_for_rfq( + self, rfq_id: str, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """Confirm a quote scoped to its RFQ (spec v3.22.0). + + ``PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm``. + """ + self._require_auth() + # json={} forces Content-Type: application/json — demo rejects empty PUTs. + await self._put( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}" + f"/quotes/{_seg(quote_id, name='quote_id')}/confirm", + json={}, + extra_headers=extra_headers, + ) + class AsyncBlockTradeProposalsResource(AsyncResource): """Async block-trade-proposals sub-resource. @@ -1700,8 +1821,6 @@ async def list_quotes( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -1717,8 +1836,6 @@ async def list_quotes( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, @@ -1736,8 +1853,6 @@ def list_all_quotes( limit: int | None = None, min_ts: int | None = None, max_ts: int | None = None, - event_ticker: str | None = None, - market_ticker: str | None = None, status: QuoteStatusLiteral | None = None, quote_creator_user_id: str | None = None, rfq_creator_user_id: str | None = None, @@ -1753,8 +1868,6 @@ def list_all_quotes( limit=limit, min_ts=min_ts, max_ts=max_ts, - event_ticker=event_ticker, - market_ticker=market_ticker, status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, diff --git a/kalshi/resources/orders.py b/kalshi/resources/orders.py index 2622cf0..9eed8d5 100644 --- a/kalshi/resources/orders.py +++ b/kalshi/resources/orders.py @@ -1,50 +1,43 @@ -"""Orders resource — create, get, cancel, list, batch operations.""" +"""Orders resource — get, list, queue positions, and V2 event-market order writes. + +The legacy V1 order-write endpoints (``POST/DELETE /portfolio/orders``, +``/portfolio/orders/batched``, ``/portfolio/orders/{id}/amend|decrease``) were +removed from the Kalshi OpenAPI spec in v3.22.0. Order writes now go exclusively +through the V2 ``/portfolio/events/orders`` family below. The read endpoints +(``get``, ``list``, ``queue_positions``) and ``fills`` are unchanged. +""" from __future__ import annotations import builtins -from collections.abc import AsyncIterator, Iterator, Sequence +from collections.abc import AsyncIterator, Iterator from decimal import Decimal -from typing import Any, overload +from typing import Any from typing_extensions import deprecated from kalshi.errors import KalshiError from kalshi.models.common import Page from kalshi.models.orders import ( - ActionLiteral, - AmendOrderRequest, - AmendOrderResponse, AmendOrderV2Request, AmendOrderV2Response, - BatchCancelOrdersRequest, - BatchCancelOrdersRequestOrder, - BatchCancelOrdersResponse, BatchCancelOrdersV2Request, BatchCancelOrdersV2Response, - BatchCreateOrdersRequest, - BatchCreateOrdersResponse, BatchCreateOrdersV2Request, BatchCreateOrdersV2Response, CancelOrderV2Response, - CreateOrderRequest, CreateOrderV2Request, CreateOrderV2Response, - DecreaseOrderRequest, DecreaseOrderV2Request, DecreaseOrderV2Response, Fill, Order, OrderQueuePosition, OrderStatusLiteral, - SelfTradePreventionTypeLiteral, - SideLiteral, - TimeInForceLiteral, ) from kalshi.resources._base import ( AsyncResource, SyncResource, - _check_request_exclusive, _fills_params, _join_tickers, _params, @@ -54,203 +47,6 @@ ) from kalshi.types import to_decimal -# --------------------------------------------------------------------------- -# Shared request-body builders (issue #46: dedup sync/async resource bodies). -# -# These helpers are pure: kwargs in, ``dict[str, Any]`` out. Both the sync and -# async resource methods call the same builder so dispatcher logic, model -# construction, and serialization live in one place. -# --------------------------------------------------------------------------- - - -def _build_create_order_body( - request: CreateOrderRequest | None, - *, - ticker: str | None, - side: SideLiteral | None, - action: ActionLiteral | None, - count: int | None, - yes_price: float | str | int | None, - no_price: float | str | int | None, - client_order_id: str | None, - expiration_ts: int | None, - buy_max_cost: int | None, - time_in_force: TimeInForceLiteral | None, - post_only: bool | None, - reduce_only: bool | None, - self_trade_prevention_type: SelfTradePreventionTypeLiteral | None, - order_group_id: str | None, - cancel_order_on_pause: bool | None, - subaccount: int | None, - exchange_index: int | None, -) -> dict[str, Any]: - _check_request_exclusive( - request, - ticker=ticker, - side=side, - action=action, - count=count, - yes_price=yes_price, - no_price=no_price, - client_order_id=client_order_id, - expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, - time_in_force=time_in_force, - post_only=post_only, - reduce_only=reduce_only, - self_trade_prevention_type=self_trade_prevention_type, - order_group_id=order_group_id, - cancel_order_on_pause=cancel_order_on_pause, - subaccount=subaccount, - exchange_index=exchange_index, - ) - if request is None: - if ticker is None or side is None or count is None or action is None: - raise TypeError( - "create() requires `ticker`, `side`, `count`, and `action` " - "(or pass `request=...`). Pre-#242 the SDK silently defaulted " - 'missing `count` to 1 contract and missing `action` to "buy" — ' - "that has been removed: a missing arg would otherwise translate " - "into a real 1-contract BUY on the wire." - ) - request = CreateOrderRequest( - ticker=ticker, - side=side, - action=action, - count=to_decimal(count), - yes_price=to_decimal(yes_price) if yes_price is not None else None, - no_price=to_decimal(no_price) if no_price is not None else None, - client_order_id=client_order_id, - expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, - time_in_force=time_in_force, - post_only=post_only, - reduce_only=reduce_only, - self_trade_prevention_type=self_trade_prevention_type, - order_group_id=order_group_id, - cancel_order_on_pause=cancel_order_on_pause, - subaccount=subaccount, - exchange_index=exchange_index, - ) - return request.model_dump(exclude_none=True, by_alias=True, mode="json") - - -def _build_batch_create_body( - request: BatchCreateOrdersRequest | None, - orders: Sequence[CreateOrderRequest] | None, -) -> bytes: - """Serialize the batch-create body directly to JSON bytes. - - Skips the intermediate ``model_dump(mode="json")`` dict-walk: with up - to 100 orders x ~10 Decimal fields, the dict version pays the - serializer cost twice (once here, once in httpx's json encoder). - """ - _check_request_exclusive(request, orders=orders) - if request is None: - if orders is None: - raise TypeError("batch_create() requires `orders` (or pass `request=...`)") - request = BatchCreateOrdersRequest(orders=list(orders)) - return request.model_dump_json(exclude_none=True, by_alias=True).encode() - - -def _build_batch_cancel_body( - request: BatchCancelOrdersRequest | None, - orders: Sequence[BatchCancelOrdersRequestOrder | str] | None, -) -> bytes: - """Serialize the batch-cancel body directly to JSON bytes. See - :func:`_build_batch_create_body` for the perf rationale.""" - _check_request_exclusive(request, orders=orders) - if request is None: - if orders is None: - raise TypeError("batch_cancel() requires `orders` (or pass `request=...`)") - normalized = [ - (BatchCancelOrdersRequestOrder(order_id=o) if isinstance(o, str) else o) for o in orders - ] - request = BatchCancelOrdersRequest(orders=normalized) - return request.model_dump_json(exclude_none=True, by_alias=True).encode() - - - -def _build_amend_body( - request: AmendOrderRequest | None, - *, - ticker: str | None, - side: SideLiteral | None, - action: ActionLiteral | None, - yes_price: float | str | int | None, - no_price: float | str | int | None, - count: int | None, - client_order_id: str | None, - updated_client_order_id: str | None, - subaccount: int | None, - exchange_index: int | None, -) -> dict[str, Any]: - _check_request_exclusive( - request, - ticker=ticker, - side=side, - action=action, - yes_price=yes_price, - no_price=no_price, - count=count, - client_order_id=client_order_id, - updated_client_order_id=updated_client_order_id, - subaccount=subaccount, - exchange_index=exchange_index, - ) - if request is None: - if ticker is None or side is None or action is None: - raise TypeError( - "amend() requires `ticker`, `side`, and `action` (or pass `request=...`)" - ) - if yes_price is None and no_price is None and count is None: - raise ValueError("amend() requires at least one of yes_price, no_price, or count") - request = AmendOrderRequest( - ticker=ticker, - side=side, - action=action, - yes_price=to_decimal(yes_price) if yes_price is not None else None, - no_price=to_decimal(no_price) if no_price is not None else None, - count=to_decimal(count) if count is not None else None, - client_order_id=client_order_id, - updated_client_order_id=updated_client_order_id, - subaccount=subaccount, - exchange_index=exchange_index, - ) - return request.model_dump(exclude_none=True, by_alias=True, mode="json") - - -def _build_decrease_body( - request: DecreaseOrderRequest | None, - *, - reduce_by: int | None, - reduce_to: int | None, - subaccount: int | None, - exchange_index: int | None, -) -> dict[str, Any]: - _check_request_exclusive( - request, - reduce_by=reduce_by, - reduce_to=reduce_to, - subaccount=subaccount, - exchange_index=exchange_index, - ) - if request is None: - # Method-level guards mirror DecreaseOrderRequest._enforce_reduce_xor - # by design: the model-first v0.9 API will rely on the model validator, - # but this path preserves nicer ValueError messages for current callers. - if reduce_by is None and reduce_to is None: - raise ValueError("decrease() requires either reduce_by or reduce_to") - if reduce_by is not None and reduce_to is not None: - raise ValueError("decrease() accepts reduce_by or reduce_to, not both") - request = DecreaseOrderRequest( - reduce_by=reduce_by, - reduce_to=reduce_to, - subaccount=subaccount, - exchange_index=exchange_index, - ) - return request.model_dump(exclude_none=True, by_alias=True, mode="json") - def _list_orders_params( *, @@ -305,103 +101,6 @@ def _parse_queue_position(data: dict[str, Any]) -> Decimal: class OrdersResource(SyncResource): """Sync orders API.""" - @overload - def create( - self, *, request: CreateOrderRequest, extra_headers: dict[str, str] | None = None - ) -> Order: ... - @overload - def create( - self, - *, - ticker: str, - side: SideLiteral, - action: ActionLiteral, - count: int, - yes_price: float | str | int | None = ..., - no_price: float | str | int | None = ..., - client_order_id: str | None = ..., - expiration_ts: int | None = ..., - buy_max_cost: int | None = ..., - time_in_force: TimeInForceLiteral | None = ..., - post_only: bool | None = ..., - reduce_only: bool | None = ..., - self_trade_prevention_type: SelfTradePreventionTypeLiteral | None = ..., - order_group_id: str | None = ..., - cancel_order_on_pause: bool | None = ..., - subaccount: int | None = ..., - exchange_index: int | None = ..., - extra_headers: dict[str, str] | None = None, - ) -> Order: ... - def create( - self, - *, - request: CreateOrderRequest | None = None, - ticker: str | None = None, - side: SideLiteral | None = None, - action: ActionLiteral | None = None, - count: int | None = None, - yes_price: float | str | int | None = None, - no_price: float | str | int | None = None, - client_order_id: str | None = None, - expiration_ts: int | None = None, - buy_max_cost: int | None = None, - time_in_force: TimeInForceLiteral | None = None, - post_only: bool | None = None, - reduce_only: bool | None = None, - self_trade_prevention_type: SelfTradePreventionTypeLiteral | None = None, - order_group_id: str | None = None, - cancel_order_on_pause: bool | None = None, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> Order: - """Place a new order. - - ``buy_max_cost`` is integer cents per OpenAPI spec (e.g., 500 for $5.00). - - ``time_in_force`` accepts ``"fill_or_kill"``, ``"good_till_canceled"``, - ``"immediate_or_cancel"``. Passing ``None`` omits the field and lets - Kalshi apply its server-side default (``good_till_canceled``). - - v0.8.0 removed the ``type`` kwarg: the field was never defined in - the OpenAPI spec. Callers passing ``type="limit"`` now get a - ``TypeError``. - - #242 (v2.5): on the kwarg path, ``count`` and ``action`` are now - REQUIRED — passing neither raises ``TypeError`` before any HTTP - request. Previously the SDK silently defaulted to ``count=1`` and - ``action="buy"``, which converted a missing-arg bug into a real - 1-contract BUY fill. The ``request=CreateOrderRequest(...)`` - overload is unaffected (the model itself now declares them required). - - v1.1 (#56): pass a pre-built ``request=CreateOrderRequest(...)`` instead - of individual kwargs. Mutually exclusive with the kwarg form. - """ - self._require_auth() - body = _build_create_order_body( - request, - ticker=ticker, - side=side, - action=action, - count=count, - yes_price=yes_price, - no_price=no_price, - client_order_id=client_order_id, - expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, - time_in_force=time_in_force, - post_only=post_only, - reduce_only=reduce_only, - self_trade_prevention_type=self_trade_prevention_type, - order_group_id=order_group_id, - cancel_order_on_pause=cancel_order_on_pause, - subaccount=subaccount, - exchange_index=exchange_index, - ) - data = self._post("/portfolio/orders", json=body, extra_headers=extra_headers) - order_data = data.get("order", data) - return Order.model_validate(order_data) - def get(self, order_id: str, *, extra_headers: dict[str, str] | None = None) -> Order: self._require_auth() data = self._get( @@ -410,22 +109,6 @@ def get(self, order_id: str, *, extra_headers: dict[str, str] | None = None) -> order_data = data.get("order", data) return Order.model_validate(order_data) - def cancel( - self, - order_id: str, - *, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> None: - self._require_auth() - params = _params(subaccount=subaccount, exchange_index=exchange_index) - self._delete( - f"/portfolio/orders/{_seg(order_id, name='order_id')}", - params=params, - extra_headers=extra_headers, - ) - def list( self, *, @@ -488,81 +171,6 @@ def list_all( extra_headers=extra_headers, ) - @overload - def batch_create( - self, *, request: BatchCreateOrdersRequest, extra_headers: dict[str, str] | None = None - ) -> BatchCreateOrdersResponse: ... - @overload - def batch_create( - self, orders: Sequence[CreateOrderRequest], *, extra_headers: dict[str, str] | None = None - ) -> BatchCreateOrdersResponse: ... - def batch_create( - self, - orders: Sequence[CreateOrderRequest] | None = None, - *, - request: BatchCreateOrdersRequest | None = None, - extra_headers: dict[str, str] | None = None, - ) -> BatchCreateOrdersResponse: - """Place a batch of orders. - - Changed in v2.4.0 (breaking): previously returned ``list[Order]`` and would - crash with ``ValidationError`` on any partially-failed batch - (the spec marks each entry's ``order`` and ``error`` as nullable; - the old ``Order.model_validate(o.get("order", o))`` blew up the - instant the server returned ``{"order": null, "error": {...}}``). - Now returns :class:`BatchCreateOrdersResponse` so callers can - pair the per-leg ``client_order_id`` with ``order``/``error``. - """ - self._require_auth() - body = _build_batch_create_body(request, orders) - data = self._post_json( - "/portfolio/orders/batched", content=body, extra_headers=extra_headers - ) - return BatchCreateOrdersResponse.model_validate(data) - - @overload - def batch_cancel( - self, *, request: BatchCancelOrdersRequest, extra_headers: dict[str, str] | None = None - ) -> BatchCancelOrdersResponse: ... - @overload - def batch_cancel( - self, - orders: Sequence[BatchCancelOrdersRequestOrder | str], - *, - extra_headers: dict[str, str] | None = None, - ) -> BatchCancelOrdersResponse: ... - def batch_cancel( - self, - orders: Sequence[BatchCancelOrdersRequestOrder | str] | None = None, - *, - request: BatchCancelOrdersRequest | None = None, - extra_headers: dict[str, str] | None = None, - ) -> BatchCancelOrdersResponse: - """Batch-cancel orders. - - Accepts a sequence of either ``BatchCancelOrdersRequestOrder`` - entries (for per-order ``subaccount`` routing), plain order-id - strings (convenience shortcut — each is wrapped internally), or a - mix of both. String entries are wrapped as - ``BatchCancelOrdersRequestOrder(order_id=)`` before serialization. - - Changed in v2.4.0 (breaking): previously returned ``None`` and discarded - the server's per-leg response. Now returns - :class:`BatchCancelOrdersResponse` so callers can read the - load-bearing ``reduced_by_fp`` per entry (cents canceled) plus - any per-leg ``error`` blocks. A server that returns 204 - No Content raises :class:`KalshiError` — every modern Kalshi - environment returns 200 with the typed body. - """ - self._require_auth() - body = _build_batch_cancel_body(request, orders) - data = self._delete_with_body_json( - "/portfolio/orders/batched", content=body, extra_headers=extra_headers - ) - if data is None: - raise KalshiError("Expected BatchCancelOrdersResponse body, got 204 No Content.") - return BatchCancelOrdersResponse.model_validate(data) - @deprecated( "OrdersResource.fills is deprecated; use client.portfolio.fills instead." ) @@ -629,115 +237,6 @@ def fills_all( extra_headers=extra_headers, ) - @overload - def amend( - self, - order_id: str, - *, - request: AmendOrderRequest, - extra_headers: dict[str, str] | None = None, - ) -> AmendOrderResponse: ... - @overload - def amend( - self, - order_id: str, - *, - ticker: str, - side: SideLiteral, - action: ActionLiteral, - yes_price: float | str | int | None = ..., - no_price: float | str | int | None = ..., - count: int | None = ..., - client_order_id: str | None = ..., - updated_client_order_id: str | None = ..., - subaccount: int | None = ..., - exchange_index: int | None = ..., - extra_headers: dict[str, str] | None = None, - ) -> AmendOrderResponse: ... - def amend( - self, - order_id: str, - *, - request: AmendOrderRequest | None = None, - ticker: str | None = None, - side: SideLiteral | None = None, - action: ActionLiteral | None = None, - yes_price: float | str | int | None = None, - no_price: float | str | int | None = None, - count: int | None = None, - client_order_id: str | None = None, - updated_client_order_id: str | None = None, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> AmendOrderResponse: - self._require_auth() - body = _build_amend_body( - request, - ticker=ticker, - side=side, - action=action, - yes_price=yes_price, - no_price=no_price, - count=count, - client_order_id=client_order_id, - updated_client_order_id=updated_client_order_id, - subaccount=subaccount, - exchange_index=exchange_index, - ) - data = self._post( - f"/portfolio/orders/{_seg(order_id, name='order_id')}/amend", - json=body, - extra_headers=extra_headers, - ) - return AmendOrderResponse.model_validate(data) - - @overload - def decrease( - self, - order_id: str, - *, - request: DecreaseOrderRequest, - extra_headers: dict[str, str] | None = None, - ) -> Order: ... - @overload - def decrease( - self, - order_id: str, - *, - reduce_by: int | None = ..., - reduce_to: int | None = ..., - subaccount: int | None = ..., - exchange_index: int | None = ..., - extra_headers: dict[str, str] | None = None, - ) -> Order: ... - def decrease( - self, - order_id: str, - *, - request: DecreaseOrderRequest | None = None, - reduce_by: int | None = None, - reduce_to: int | None = None, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> Order: - self._require_auth() - body = _build_decrease_body( - request, - reduce_by=reduce_by, - reduce_to=reduce_to, - subaccount=subaccount, - exchange_index=exchange_index, - ) - data = self._post( - f"/portfolio/orders/{_seg(order_id, name='order_id')}/decrease", - json=body, - extra_headers=extra_headers, - ) - order_data = data.get("order", data) - return Order.model_validate(order_data) - def queue_positions( self, *, @@ -787,10 +286,18 @@ def cancel_v2( *, subaccount: int | None = None, exchange_index: int | None = None, + market_ticker: str | None = None, extra_headers: dict[str, str] | None = None, ) -> CancelOrderV2Response: + """Cancel an event-market order (V2). + + ``market_ticker`` (spec v3.22.0) is required when ``exchange_index`` + is ``-1`` (auto-route by ticker); omit it otherwise. + """ self._require_auth() - params = _params(subaccount=subaccount, exchange_index=exchange_index) + params = _params( + subaccount=subaccount, exchange_index=exchange_index, market_ticker=market_ticker + ) data = self._delete( f"/portfolio/events/orders/{_seg(order_id, name='order_id')}", params=params, @@ -879,103 +386,6 @@ def batch_cancel_v2( class AsyncOrdersResource(AsyncResource): """Async orders API.""" - @overload - async def create( - self, *, request: CreateOrderRequest, extra_headers: dict[str, str] | None = None - ) -> Order: ... - @overload - async def create( - self, - *, - ticker: str, - side: SideLiteral, - action: ActionLiteral, - count: int, - yes_price: float | str | int | None = ..., - no_price: float | str | int | None = ..., - client_order_id: str | None = ..., - expiration_ts: int | None = ..., - buy_max_cost: int | None = ..., - time_in_force: TimeInForceLiteral | None = ..., - post_only: bool | None = ..., - reduce_only: bool | None = ..., - self_trade_prevention_type: SelfTradePreventionTypeLiteral | None = ..., - order_group_id: str | None = ..., - cancel_order_on_pause: bool | None = ..., - subaccount: int | None = ..., - exchange_index: int | None = ..., - extra_headers: dict[str, str] | None = None, - ) -> Order: ... - async def create( - self, - *, - request: CreateOrderRequest | None = None, - ticker: str | None = None, - side: SideLiteral | None = None, - action: ActionLiteral | None = None, - count: int | None = None, - yes_price: float | str | int | None = None, - no_price: float | str | int | None = None, - client_order_id: str | None = None, - expiration_ts: int | None = None, - buy_max_cost: int | None = None, - time_in_force: TimeInForceLiteral | None = None, - post_only: bool | None = None, - reduce_only: bool | None = None, - self_trade_prevention_type: SelfTradePreventionTypeLiteral | None = None, - order_group_id: str | None = None, - cancel_order_on_pause: bool | None = None, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> Order: - """Place a new order. - - ``buy_max_cost`` is integer cents per OpenAPI spec (e.g., 500 for $5.00). - - ``time_in_force`` accepts ``"fill_or_kill"``, ``"good_till_canceled"``, - ``"immediate_or_cancel"``. Passing ``None`` omits the field and lets - Kalshi apply its server-side default (``good_till_canceled``). - - v0.8.0 removed the ``type`` kwarg: the field was never defined in - the OpenAPI spec. Callers passing ``type="limit"`` now get a - ``TypeError``. - - #242 (v2.5): on the kwarg path, ``count`` and ``action`` are now - REQUIRED — passing neither raises ``TypeError`` before any HTTP - request. Previously the SDK silently defaulted to ``count=1`` and - ``action="buy"``, which converted a missing-arg bug into a real - 1-contract BUY fill. The ``request=CreateOrderRequest(...)`` - overload is unaffected (the model itself now declares them required). - - v1.1 (#56): pass a pre-built ``request=CreateOrderRequest(...)`` instead - of individual kwargs. Mutually exclusive with the kwarg form. - """ - self._require_auth() - body = _build_create_order_body( - request, - ticker=ticker, - side=side, - action=action, - count=count, - yes_price=yes_price, - no_price=no_price, - client_order_id=client_order_id, - expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, - time_in_force=time_in_force, - post_only=post_only, - reduce_only=reduce_only, - self_trade_prevention_type=self_trade_prevention_type, - order_group_id=order_group_id, - cancel_order_on_pause=cancel_order_on_pause, - subaccount=subaccount, - exchange_index=exchange_index, - ) - data = await self._post("/portfolio/orders", json=body, extra_headers=extra_headers) - order_data = data.get("order", data) - return Order.model_validate(order_data) - async def get(self, order_id: str, *, extra_headers: dict[str, str] | None = None) -> Order: self._require_auth() data = await self._get( @@ -984,22 +394,6 @@ async def get(self, order_id: str, *, extra_headers: dict[str, str] | None = Non order_data = data.get("order", data) return Order.model_validate(order_data) - async def cancel( - self, - order_id: str, - *, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> None: - self._require_auth() - params = _params(subaccount=subaccount, exchange_index=exchange_index) - await self._delete( - f"/portfolio/orders/{_seg(order_id, name='order_id')}", - params=params, - extra_headers=extra_headers, - ) - async def list( self, *, @@ -1063,57 +457,6 @@ def list_all( extra_headers=extra_headers, ) - @overload - async def batch_create( - self, *, request: BatchCreateOrdersRequest, extra_headers: dict[str, str] | None = None - ) -> BatchCreateOrdersResponse: ... - @overload - async def batch_create( - self, orders: Sequence[CreateOrderRequest], *, extra_headers: dict[str, str] | None = None - ) -> BatchCreateOrdersResponse: ... - async def batch_create( - self, - orders: Sequence[CreateOrderRequest] | None = None, - *, - request: BatchCreateOrdersRequest | None = None, - extra_headers: dict[str, str] | None = None, - ) -> BatchCreateOrdersResponse: - """Place a batch of orders. See :meth:`OrdersResource.batch_create`.""" - self._require_auth() - body = _build_batch_create_body(request, orders) - data = await self._post_json( - "/portfolio/orders/batched", content=body, extra_headers=extra_headers - ) - return BatchCreateOrdersResponse.model_validate(data) - - @overload - async def batch_cancel( - self, *, request: BatchCancelOrdersRequest, extra_headers: dict[str, str] | None = None - ) -> BatchCancelOrdersResponse: ... - @overload - async def batch_cancel( - self, - orders: Sequence[BatchCancelOrdersRequestOrder | str], - *, - extra_headers: dict[str, str] | None = None, - ) -> BatchCancelOrdersResponse: ... - async def batch_cancel( - self, - orders: Sequence[BatchCancelOrdersRequestOrder | str] | None = None, - *, - request: BatchCancelOrdersRequest | None = None, - extra_headers: dict[str, str] | None = None, - ) -> BatchCancelOrdersResponse: - """Batch-cancel orders. See :meth:`OrdersResource.batch_cancel`.""" - self._require_auth() - body = _build_batch_cancel_body(request, orders) - data = await self._delete_with_body_json( - "/portfolio/orders/batched", content=body, extra_headers=extra_headers - ) - if data is None: - raise KalshiError("Expected BatchCancelOrdersResponse body, got 204 No Content.") - return BatchCancelOrdersResponse.model_validate(data) - @deprecated( "AsyncOrdersResource.fills is deprecated; use client.portfolio.fills instead." ) @@ -1180,115 +523,6 @@ def fills_all( extra_headers=extra_headers, ) - @overload - async def amend( - self, - order_id: str, - *, - request: AmendOrderRequest, - extra_headers: dict[str, str] | None = None, - ) -> AmendOrderResponse: ... - @overload - async def amend( - self, - order_id: str, - *, - ticker: str, - side: SideLiteral, - action: ActionLiteral, - yes_price: float | str | int | None = ..., - no_price: float | str | int | None = ..., - count: int | None = ..., - client_order_id: str | None = ..., - updated_client_order_id: str | None = ..., - subaccount: int | None = ..., - exchange_index: int | None = ..., - extra_headers: dict[str, str] | None = None, - ) -> AmendOrderResponse: ... - async def amend( - self, - order_id: str, - *, - request: AmendOrderRequest | None = None, - ticker: str | None = None, - side: SideLiteral | None = None, - action: ActionLiteral | None = None, - yes_price: float | str | int | None = None, - no_price: float | str | int | None = None, - count: int | None = None, - client_order_id: str | None = None, - updated_client_order_id: str | None = None, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> AmendOrderResponse: - self._require_auth() - body = _build_amend_body( - request, - ticker=ticker, - side=side, - action=action, - yes_price=yes_price, - no_price=no_price, - count=count, - client_order_id=client_order_id, - updated_client_order_id=updated_client_order_id, - subaccount=subaccount, - exchange_index=exchange_index, - ) - data = await self._post( - f"/portfolio/orders/{_seg(order_id, name='order_id')}/amend", - json=body, - extra_headers=extra_headers, - ) - return AmendOrderResponse.model_validate(data) - - @overload - async def decrease( - self, - order_id: str, - *, - request: DecreaseOrderRequest, - extra_headers: dict[str, str] | None = None, - ) -> Order: ... - @overload - async def decrease( - self, - order_id: str, - *, - reduce_by: int | None = ..., - reduce_to: int | None = ..., - subaccount: int | None = ..., - exchange_index: int | None = ..., - extra_headers: dict[str, str] | None = None, - ) -> Order: ... - async def decrease( - self, - order_id: str, - *, - request: DecreaseOrderRequest | None = None, - reduce_by: int | None = None, - reduce_to: int | None = None, - subaccount: int | None = None, - exchange_index: int | None = None, - extra_headers: dict[str, str] | None = None, - ) -> Order: - self._require_auth() - body = _build_decrease_body( - request, - reduce_by=reduce_by, - reduce_to=reduce_to, - subaccount=subaccount, - exchange_index=exchange_index, - ) - data = await self._post( - f"/portfolio/orders/{_seg(order_id, name='order_id')}/decrease", - json=body, - extra_headers=extra_headers, - ) - order_data = data.get("order", data) - return Order.model_validate(order_data) - async def queue_positions( self, *, @@ -1335,10 +569,18 @@ async def cancel_v2( *, subaccount: int | None = None, exchange_index: int | None = None, + market_ticker: str | None = None, extra_headers: dict[str, str] | None = None, ) -> CancelOrderV2Response: + """Cancel an event-market order (V2). + + ``market_ticker`` (spec v3.22.0) is required when ``exchange_index`` + is ``-1`` (auto-route by ticker); omit it otherwise. + """ self._require_auth() - params = _params(subaccount=subaccount, exchange_index=exchange_index) + params = _params( + subaccount=subaccount, exchange_index=exchange_index, market_ticker=market_ticker + ) data = await self._delete( f"/portfolio/events/orders/{_seg(order_id, name='order_id')}", params=params, diff --git a/pyproject.toml b/pyproject.toml index 9e4f664..ba08e6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "4.2.0" +version = "5.0.0" description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs" readme = "README.md" license = { text = "MIT" } diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 72c714d..37934a8 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -1,9 +1,10 @@ openapi: 3.0.0 info: title: Kalshi Trade API Manual Endpoints - version: 3.21.0 - description: Manually defined OpenAPI spec for endpoints being migrated to spec-first approach - + version: 3.22.0 + description: >- + Manually defined OpenAPI spec for endpoints being migrated to spec-first + approach servers: - url: https://external-api.kalshi.com/trade-api/v2 description: Production Trade API server @@ -13,7 +14,6 @@ servers: description: Demo Trade API server - url: https://demo-api.kalshi.co/trade-api/v2 description: Demo shared API server, also supported - paths: /exchange/status: get: @@ -47,7 +47,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ExchangeStatus' - /exchange/announcements: get: operationId: GetExchangeAnnouncements @@ -64,7 +63,6 @@ paths: $ref: '#/components/schemas/GetExchangeAnnouncementsResponse' '500': description: Internal server error - /series/fee_changes: get: operationId: GetSeriesFeeChanges @@ -96,7 +94,6 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /exchange/schedule: get: operationId: GetExchangeSchedule @@ -113,7 +110,6 @@ paths: $ref: '#/components/schemas/GetExchangeScheduleResponse' '500': description: Internal server error - /exchange/user_data_timestamp: get: operationId: GetUserDataTimestamp @@ -130,14 +126,19 @@ paths: $ref: '#/components/schemas/GetUserDataTimestampResponse' '500': description: Internal server error - /series/{series_ticker}/markets/{ticker}/candlesticks: get: operationId: GetMarketCandlesticks summary: Get Market Candlesticks - description: | - Time period length of each candlestick in minutes. Valid values: 1 (1 minute), 60 (1 hour), 1440 (1 day). - Candlesticks for markets that settled before the historical cutoff are only available via `GET /historical/markets/{ticker}/candlesticks`. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details. + description: > + Time period length of each candlestick in minutes. Valid values: 1 (1 + minute), 60 (1 hour), 1440 (1 day). + + Candlesticks for markets that settled before the historical cutoff are + only available via `GET /historical/markets/{ticker}/candlesticks`. See + [Historical + Data](https://docs.kalshi.com/getting_started/historical_data) for + details. tags: - market parameters: @@ -156,34 +157,49 @@ paths: - name: start_ts in: query required: true - description: Start timestamp (Unix timestamp). Candlesticks will include those ending on or after this time. + description: >- + Start timestamp (Unix timestamp). Candlesticks will include those + ending on or after this time. schema: type: integer format: int64 - name: end_ts in: query required: true - description: End timestamp (Unix timestamp). Candlesticks will include those ending on or before this time. + description: >- + End timestamp (Unix timestamp). Candlesticks will include those + ending on or before this time. schema: type: integer format: int64 - name: period_interval in: query required: true - description: Time period length of each candlestick in minutes. Valid values are 1 (1 minute), 60 (1 hour), or 1440 (1 day). + description: >- + Time period length of each candlestick in minutes. Valid values are + 1 (1 minute), 60 (1 hour), or 1440 (1 day). schema: type: integer - enum: [1, 60, 1440] + enum: + - 1 + - 60 + - 1440 x-oapi-codegen-extra-tags: - validate: "required,oneof=1 60 1440" + validate: required,oneof=1 60 1440 - name: include_latest_before_start in: query required: false - description: | - If true, prepends the latest candlestick available before the start_ts. This synthetic candlestick is created by: + description: > + If true, prepends the latest candlestick available before the + start_ts. This synthetic candlestick is created by: + 1. Finding the most recent real candlestick before start_ts - 2. Projecting it forward to the first period boundary (calculated as the next period interval after start_ts) - 3. Setting all OHLC prices to null, and `previous_price` to the close price from the real candlestick + + 2. Projecting it forward to the first period boundary (calculated as + the next period interval after start_ts) + + 3. Setting all OHLC prices to null, and `previous_price` to the + close price from the real candlestick schema: type: boolean default: false @@ -200,13 +216,21 @@ paths: description: Not found '500': description: Internal server error - /markets/trades: get: operationId: GetTrades summary: Get Trades - description: | - Endpoint for getting all trades for all markets. A trade represents a completed transaction between two users on a specific market. Each trade includes the market ticker, price, quantity, and timestamp information. Block trades are included in the response by default and identified by the `is_block_trade` field; use the `is_block_trade` query parameter to filter by block / non-block. This endpoint returns a paginated response. Use the 'limit' parameter to control page size (1-1000, defaults to 100). The response includes a 'cursor' field - pass this value in the 'cursor' parameter of your next request to get the next page. An empty cursor indicates no more pages are available. + description: > + Endpoint for getting all trades for all markets. A trade represents a + completed transaction between two users on a specific market. Each trade + includes the market ticker, price, quantity, and timestamp information. + Block trades are included in the response by default and identified by + the `is_block_trade` field; use the `is_block_trade` query parameter to + filter by block / non-block. This endpoint returns a paginated response. + Use the 'limit' parameter to control page size (1-1000, defaults to + 100). The response includes a 'cursor' field - pass this value in the + 'cursor' parameter of your next request to get the next page. An empty + cursor indicates no more pages are available. tags: - market parameters: @@ -227,7 +251,6 @@ paths: description: Bad request '500': description: Internal server error - /markets/{ticker}/orderbook: get: operationId: GetMarketOrderbook @@ -243,7 +266,9 @@ paths: - $ref: '#/components/parameters/TickerPath' - name: depth in: query - description: Depth of the orderbook to retrieve (0 or negative means all levels, 1-100 for specific depth) + description: >- + Depth of the orderbook to retrieve (0 or negative means all levels, + 1-100 for specific depth) required: false schema: type: integer @@ -265,12 +290,20 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /markets/orderbooks: get: operationId: GetMarketOrderbooks summary: Get Multiple Market Orderbooks - description: 'Endpoint for getting the current order books for multiple markets in a single request. The order book shows all active bid orders for both yes and no sides of a binary market. It returns yes bids and no bids only (no asks are returned). This is because in binary markets, a bid for yes at price X is equivalent to an ask for no at price (100-X). For example, a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract sizes. Each side shows price levels with their corresponding quantities and order counts, organized from best to worst prices. Returns one orderbook per requested market ticker.' + description: >- + Endpoint for getting the current order books for multiple markets in a + single request. The order book shows all active bid orders for both yes + and no sides of a binary market. It returns yes bids and no bids only + (no asks are returned). This is because in binary markets, a bid for yes + at price X is equivalent to an ask for no at price (100-X). For example, + a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract + sizes. Each side shows price levels with their corresponding quantities + and order counts, organized from best to worst prices. Returns one + orderbook per requested market ticker. tags: - market security: @@ -306,7 +339,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /series/{series_ticker}: get: operationId: GetSeries @@ -328,7 +360,9 @@ paths: type: boolean default: false x-go-type-skip-optional-pointer: true - description: If true, includes the total volume traded across all events in this series. + description: >- + If true, includes the total volume traded across all events in this + series. responses: '200': description: Series retrieved successfully @@ -340,7 +374,6 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /series: get: operationId: GetSeriesList @@ -375,11 +408,15 @@ paths: type: boolean default: false x-go-type-skip-optional-pointer: true - description: If true, includes the total volume traded across all events in each series. + description: >- + If true, includes the total volume traded across all events in each + series. - name: min_updated_ts in: query required: false - description: Filter series with metadata updated after this Unix timestamp (in seconds). Use this to efficiently poll for changes. + description: >- + Filter series with metadata updated after this Unix timestamp (in + seconds). Use this to efficiently poll for changes. schema: type: integer format: int64 @@ -394,25 +431,24 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /markets: get: operationId: GetMarkets summary: Get Markets - description: | - Filter by market status. Possible values: `unopened`, `open`, `closed`, `settled`. Leave empty to return markets with any status. - - Only one `status` filter may be supplied at a time. - - Timestamp filters will be mutually exclusive from other timestamp filters and certain status filters. - - | Compatible Timestamp Filters | Additional Status Filters| Extra Notes | - |------------------------------|--------------------------|-------------| - | min_created_ts, max_created_ts | `unopened`, `open`, *empty* | | - | min_close_ts, max_close_ts | `closed`, *empty* | | - | min_settled_ts, max_settled_ts | `settled`, *empty* | | - | min_updated_ts | *empty* | Incompatible with all filters besides `mve_filter=exclude`. May be combined with `series_ticker`, which requires `mve_filter=exclude` | - - Markets that settled before the historical cutoff are only available via `GET /historical/markets`. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details. - + description: > + Filter by market status. Possible values: `unopened`, `open`, `closed`, + `settled`. Leave empty to return markets with any status. + - Only one `status` filter may be supplied at a time. + - Timestamp filters will be mutually exclusive from other timestamp filters and certain status filters. + + | Compatible Timestamp Filters | Additional Status Filters| Extra Notes | + |------------------------------|--------------------------|-------------| + | min_created_ts, max_created_ts | `unopened`, `open`, *empty* | | + | min_close_ts, max_close_ts | `closed`, *empty* | | + | min_settled_ts, max_settled_ts | `settled`, *empty* | | + | min_updated_ts | *empty* | Incompatible with all filters besides `mve_filter=exclude`. May be combined with `series_ticker`, which requires `mve_filter=exclude` | + + Markets that settled before the historical cutoff are only available via `GET /historical/markets`. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details. tags: - market parameters: @@ -443,7 +479,6 @@ paths: description: Unauthorized '500': description: Internal server error - /markets/{ticker}: get: operationId: GetMarket @@ -466,18 +501,22 @@ paths: description: Not found '500': description: Internal server error - /markets/candlesticks: get: operationId: BatchGetMarketCandlesticks summary: Batch Get Market Candlesticks - description: | + description: > Endpoint for retrieving candlestick data for multiple markets. + - Accepts up to 100 market tickers per request + - Returns up to 10,000 candlesticks total across all markets + - Returns candlesticks grouped by market_id - - Optionally includes a synthetic initial candlestick for price continuity (see `include_latest_before_start` parameter) + + - Optionally includes a synthetic initial candlestick for price + continuity (see `include_latest_before_start` parameter) tags: - market parameters: @@ -512,11 +551,17 @@ paths: - name: include_latest_before_start in: query required: false - description: | - If true, prepends the latest candlestick available before the start_ts. This synthetic candlestick is created by: + description: > + If true, prepends the latest candlestick available before the + start_ts. This synthetic candlestick is created by: + 1. Finding the most recent real candlestick before start_ts - 2. Projecting it forward to the first period boundary (calculated as the next period interval after start_ts) - 3. Setting all OHLC prices to null, and `previous_price` to the close price from the real candlestick + + 2. Projecting it forward to the first period boundary (calculated as + the next period interval after start_ts) + + 3. Setting all OHLC prices to null, and `previous_price` to the + close price from the real candlestick schema: type: boolean default: false @@ -533,7 +578,6 @@ paths: description: Unauthorized '500': description: Internal server error - /series/{series_ticker}/events/{ticker}/candlesticks: get: operationId: GetMarketCandlesticksByEvent @@ -562,7 +606,7 @@ paths: type: integer format: int64 x-oapi-codegen-extra-tags: - validate: "required" + validate: required - name: end_ts in: query required: true @@ -571,17 +615,22 @@ paths: type: integer format: int64 x-oapi-codegen-extra-tags: - validate: "required" + validate: required - name: period_interval in: query required: true - description: Specifies the length of each candlestick period, in minutes. Must be one minute, one hour, or one day. + description: >- + Specifies the length of each candlestick period, in minutes. Must be + one minute, one hour, or one day. schema: type: integer format: int32 - enum: [1, 60, 1440] + enum: + - 1 + - 60 + - 1440 x-oapi-codegen-extra-tags: - validate: "required,oneof=1 60 1440" + validate: required,oneof=1 60 1440 responses: '200': description: Event candlesticks retrieved successfully @@ -595,22 +644,27 @@ paths: description: Unauthorized '500': description: Internal server error - /events: get: operationId: GetEvents summary: Get Events - description: | + description: > Get all events. This endpoint excludes multivariate events. - To retrieve multivariate events, use the GET /events/multivariate endpoint. - All events are accessible through this endpoint, even if their associated markets are older than the historical cutoff. + + To retrieve multivariate events, use the GET /events/multivariate + endpoint. + + All events are accessible through this endpoint, even if their + associated markets are older than the historical cutoff. tags: - events parameters: - name: limit in: query required: false - description: Parameter to specify the number of results per page. Defaults to 200. Maximum value is 200. + description: >- + Parameter to specify the number of results per page. Defaults to + 200. Maximum value is 200. schema: type: integer minimum: 1 @@ -619,13 +673,21 @@ paths: - name: cursor in: query required: false - description: Parameter to specify the pagination cursor. Use the cursor value returned from the previous response to get the next page of results. Leave empty for the first page. + description: >- + Parameter to specify the pagination cursor. Use the cursor value + returned from the previous response to get the next page of results. + Leave empty for the first page. schema: type: string - name: with_nested_markets in: query required: false - description: Parameter to specify if nested markets should be included in the response. When true, each event will include a 'markets' field containing a list of Market objects associated with that event. Historical markets settled before the historical cutoff will not be included. + description: >- + Parameter to specify if nested markets should be included in the + response. When true, each event will include a 'markets' field + containing a list of Market objects associated with that event. + Historical markets settled before the historical cutoff will not be + included. schema: type: boolean default: false @@ -641,23 +703,33 @@ paths: - name: status in: query required: false - description: Filter by event status. Possible values are 'unopened', 'open', 'closed', 'settled'. Leave empty to return events with any status. + description: >- + Filter by event status. Possible values are 'unopened', 'open', + 'closed', 'settled'. Leave empty to return events with any status. schema: type: string - enum: ['unopened', 'open', 'closed', 'settled'] + enum: + - unopened + - open + - closed + - settled - $ref: '#/components/parameters/SeriesTickerQuery' - $ref: '#/components/parameters/EventTickersQuery' - name: min_close_ts in: query required: false - description: Filter events with at least one market with close timestamp greater than this Unix timestamp (in seconds). + description: >- + Filter events with at least one market with close timestamp greater + than this Unix timestamp (in seconds). schema: type: integer format: int64 - name: min_updated_ts in: query required: false - description: Filter events with metadata updated after this Unix timestamp (in seconds). Use this to efficiently poll for changes. + description: >- + Filter events with metadata updated after this Unix timestamp (in + seconds). Use this to efficiently poll for changes. schema: type: integer format: int64 @@ -674,12 +746,14 @@ paths: description: Unauthorized '500': description: Internal server error - /events/multivariate: get: operationId: GetMultivariateEvents summary: Get Multivariate Events - description: 'Retrieve multivariate (combo) events. These are dynamically created events from multivariate event collections. Supports filtering by series and collection ticker.' + description: >- + Retrieve multivariate (combo) events. These are dynamically created + events from multivariate event collections. Supports filtering by series + and collection ticker. tags: - events parameters: @@ -695,20 +769,28 @@ paths: - name: cursor in: query required: false - description: Pagination cursor. Use the cursor value returned from the previous response to get the next page of results. + description: >- + Pagination cursor. Use the cursor value returned from the previous + response to get the next page of results. schema: type: string - $ref: '#/components/parameters/SeriesTickerQuery' - name: collection_ticker in: query required: false - description: Filter events by collection ticker. Returns only multivariate events belonging to the specified collection. Cannot be used together with series_ticker. + description: >- + Filter events by collection ticker. Returns only multivariate events + belonging to the specified collection. Cannot be used together with + series_ticker. schema: type: string - name: with_nested_markets in: query required: false - description: Parameter to specify if nested markets should be included in the response. When true, each event will include a 'markets' field containing a list of Market objects associated with that event. + description: >- + Parameter to specify if nested markets should be included in the + response. When true, each event will include a 'markets' field + containing a list of Market objects associated with that event. schema: type: boolean default: false @@ -725,13 +807,14 @@ paths: description: Unauthorized '500': description: Internal server error - /events/fee_changes: get: operationId: GetEventFeeChanges summary: Get Event Fee Changes - description: | - Event fees are an override layered on top of the parent series' fee structure. If `fee_type_override` and `fee_multiplier_override` are null, that indicates the override is cleared. + description: > + Event fees are an override layered on top of the parent series' fee + structure. If `fee_type_override` and `fee_multiplier_override` are + null, that indicates the override is cleared. tags: - events parameters: @@ -754,15 +837,20 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /events/{event_ticker}: get: operationId: GetEvent summary: Get Event - description: | - Endpoint for getting data about an event by its ticker. An event represents a real-world occurrence that can be traded on, such as an election, sports game, or economic indicator release. - Events contain one or more markets where users can place trades on different outcomes. - All events are accessible through this endpoint, even if their associated markets are older than the historical cutoff. + description: > + Endpoint for getting data about an event by its ticker. An event + represents a real-world occurrence that can be traded on, such as an + election, sports game, or economic indicator release. + + Events contain one or more markets where users can place trades on + different outcomes. + + All events are accessible through this endpoint, even if their + associated markets are older than the historical cutoff. tags: - events parameters: @@ -775,7 +863,11 @@ paths: - name: with_nested_markets in: query required: false - description: If true, markets are included within the event object. If false (default), markets are returned as a separate top-level field in the response. Historical markets settled before the historical cutoff will not be included. + description: >- + If true, markets are included within the event object. If false + (default), markets are returned as a separate top-level field in the + response. Historical markets settled before the historical cutoff + will not be included. schema: type: boolean default: false @@ -789,13 +881,12 @@ paths: $ref: '#/components/schemas/GetEventResponse' '400': description: Bad request - '404': - description: Event not found '401': description: Unauthorized + '404': + description: Event not found '500': description: Internal server error - /events/{event_ticker}/metadata: get: operationId: GetEventMetadata @@ -819,18 +910,19 @@ paths: $ref: '#/components/schemas/GetEventMetadataResponse' '400': description: Bad request - '404': - description: Event not found '401': description: Unauthorized + '404': + description: Event not found '500': description: Internal server error - /series/{series_ticker}/events/{ticker}/forecast_percentile_history: get: operationId: GetEventForecastPercentilesHistory summary: Get Event Forecast Percentile History - description: Endpoint for getting the historical raw and formatted forecast numbers for an event at specific percentiles. + description: >- + Endpoint for getting the historical raw and formatted forecast numbers + for an event at specific percentiles. tags: - events security: @@ -881,32 +973,44 @@ paths: - name: period_interval in: query required: true - description: Specifies the length of each forecast period, in minutes. 0 for 5-second intervals, or 1, 60, or 1440 for minute-based intervals. + description: >- + Specifies the length of each forecast period, in minutes. 0 for + 5-second intervals, or 1, 60, or 1440 for minute-based intervals. schema: type: integer format: int32 - enum: [0, 1, 60, 1440] + enum: + - 0 + - 1 + - 60 + - 1440 responses: '200': description: Event forecast percentile history retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetEventForecastPercentilesHistoryResponse' + $ref: >- + #/components/schemas/GetEventForecastPercentilesHistoryResponse '400': description: Bad request '401': description: Unauthorized '500': description: Internal server error - /portfolio/orders: get: operationId: GetOrders summary: Get Orders - description: | - Restricts the response to orders that have a certain status: resting, canceled, or executed. - Orders that have been canceled or fully executed before the historical cutoff are only available via `GET /historical/orders`. Resting orders will always be available through this endpoint. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details. + description: > + Restricts the response to orders that have a certain status: resting, + canceled, or executed. + + Orders that have been canceled or fully executed before the historical + cutoff are only available via `GET /historical/orders`. Resting orders + will always be available through this endpoint. See [Historical + Data](https://docs.kalshi.com/getting_started/historical_data) for + details. tags: - orders security: @@ -935,195 +1039,19 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - - post: - operationId: CreateOrder - summary: Create Order - description: ' Endpoint for submitting orders in a market. Each user is limited to 200 000 open orders at a time.' - x-mint: - content: | - - **Rate limit:** 100 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - - tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateOrderRequest' - responses: - '201': - description: Order created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/CreateOrderResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '409': - $ref: '#/components/responses/ConflictError' - '429': - $ref: '#/components/responses/RateLimitError' - '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/orders/{order_id}: get: operationId: GetOrder summary: Get Order description: ' Endpoint for getting a single order.' x-mint: - content: | - - **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - - tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/OrderIdPath' - responses: - '200': - description: Order retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetOrderResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - - delete: - operationId: CancelOrder - summary: Cancel Order - description: ' Endpoint for canceling orders. The value for the orderId should match the id field of the order you want to decrease. Commonly, DELETE-type endpoints return 204 status with no body content on success. But we can''t completely delete the order, as it may be partially filled already. Instead, the DeleteOrder endpoint reduce the order completely, essentially zeroing the remaining resting contracts on it. The zeroed order is returned on the response payload as a form of validation for the client.' - x-mint: - content: | - - **Rate limit:** 2 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - - tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/OrderIdPath' - - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' - - $ref: '#/components/parameters/ExchangeIndexQuery' - responses: - '200': - description: Order cancelled successfully - content: - application/json: - schema: - $ref: '#/components/schemas/CancelOrderResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - - /portfolio/orders/batched: - post: - operationId: BatchCreateOrders - summary: Batch Create Orders - description: 'Endpoint for submitting a batch of orders. The maximum batch size scales with your tier''s write budget — see [Rate Limits and Tiers](/getting_started/rate_limits).' - x-mint: - content: | + content: > - **Rate limit:** 10 tokens per order in the batch — billed per item, so total cost for a batch of N orders is N × 10. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - - tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCreateOrdersRequest' - responses: - '201': - description: Batch order creation completed - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCreateOrdersResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: BatchCancelOrders - summary: Batch Cancel Orders - description: 'Endpoint for cancelling a batch of orders. The maximum batch size scales with your tier''s write budget — see [Rate Limits and Tiers](/getting_started/rate_limits).' - x-mint: - content: | - - **Rate limit:** 2 tokens per order in the batch — billed per item, so total cost for a batch of N cancels is N × 2. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - - tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCancelOrdersRequest' - responses: - '200': - description: Batch order cancellation completed - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCancelOrdersResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '500': - $ref: '#/components/responses/InternalServerError' + **Rate limit:** 2 tokens per request. See `GET + /trade-api/v2/account/endpoint_costs` for current non-default endpoint + costs. - /portfolio/orders/{order_id}/amend: - post: - operationId: AmendOrder - summary: Amend Order - description: ' Endpoint for amending the max number of fillable contracts and/or price in an existing order. Max fillable contracts is `remaining_count` + `fill_count`.' - x-mint: - content: | - - Amending a resting order preserves queue position only when the amendment decreases size. All other amendments — like increasing size or changing price forfeit queue position and place the order at the back of the queue. tags: - orders @@ -1133,68 +1061,19 @@ paths: kalshiAccessTimestamp: [] parameters: - $ref: '#/components/parameters/OrderIdPath' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AmendOrderRequest' responses: '200': - description: Order amended successfully - content: - application/json: - schema: - $ref: '#/components/schemas/AmendOrderResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - - /portfolio/orders/{order_id}/decrease: - post: - operationId: DecreaseOrder - summary: Decrease Order - description: ' Endpoint for decreasing the number of contracts in an existing order. This is the only kind of edit available on order quantity. Cancelling an order is equivalent to decreasing an order amount to zero.' - x-mint: - content: | - - **Rate limit:** 100 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. - - tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/OrderIdPath' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DecreaseOrderRequest' - responses: - '200': - description: Order decreased successfully + description: Order retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/DecreaseOrderResponse' - '400': - $ref: '#/components/responses/BadRequestError' + $ref: '#/components/schemas/GetOrderResponse' '401': $ref: '#/components/responses/UnauthorizedError' '404': $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/orders/queue_positions: get: operationId: GetOrderQueuePositions @@ -1231,7 +1110,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/orders/{order_id}/queue_position: get: operationId: GetOrderQueuePosition @@ -1258,12 +1136,16 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/events/orders: post: operationId: CreateOrderV2 summary: Create Order (V2) - description: 'Endpoint for submitting event-market orders using the V2 request/response shape (single-book `bid`/`ask` side and fixed-point dollar prices). The legacy `/portfolio/orders` endpoint will be deprecated no earlier than May 6, 2026 — clients should migrate to this path.' + description: >- + Endpoint for submitting event-market orders using the V2 + request/response shape (single-book `bid`/`ask` side and fixed-point + dollar prices). The legacy `/portfolio/orders` endpoint will be + deprecated no earlier than May 6, 2026 — clients should migrate to this + path. tags: - orders security: @@ -1293,16 +1175,24 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/events/orders/batched: post: operationId: BatchCreateOrdersV2 summary: Batch Create Orders (V2) - description: 'Endpoint for submitting a batch of event-market orders using the V2 request/response shape. The maximum batch size scales with your tier''s write budget — see [Rate Limits and Tiers](/getting_started/rate_limits).' + description: >- + Endpoint for submitting a batch of event-market orders using the V2 + request/response shape. The maximum batch size scales with your tier's + write budget — see [Rate Limits and + Tiers](/getting_started/rate_limits). x-mint: - content: | + content: > - **Rate limit:** 10 tokens per order in the batch — billed per item, so total cost for a batch of N orders is N × 10. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + + **Rate limit:** 10 tokens per order in the batch — billed per item, so + total cost for a batch of N orders is N × 10. See `GET + /trade-api/v2/account/endpoint_costs` for current non-default endpoint + costs. + tags: - orders @@ -1331,15 +1221,22 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' - delete: operationId: BatchCancelOrdersV2 summary: Batch Cancel Orders (V2) - description: 'Endpoint for cancelling a batch of event-market orders using the V2 response shape. The maximum batch size scales with your tier''s write budget — see [Rate Limits and Tiers](/getting_started/rate_limits).' + description: >- + Endpoint for cancelling a batch of event-market orders using the V2 + response shape. The maximum batch size scales with your tier's write + budget — see [Rate Limits and Tiers](/getting_started/rate_limits). x-mint: - content: | + content: > - **Rate limit:** 2 tokens per order in the batch — billed per item, so total cost for a batch of N cancels is N × 2. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + + **Rate limit:** 2 tokens per order in the batch — billed per item, so + total cost for a batch of N cancels is N × 2. See `GET + /trade-api/v2/account/endpoint_costs` for current non-default endpoint + costs. + tags: - orders @@ -1368,16 +1265,22 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/events/orders/{order_id}: delete: operationId: CancelOrderV2 summary: Cancel Order (V2) - description: 'Endpoint for cancelling event-market orders using the V2 response shape. Returns `{order_id, client_order_id, reduced_by}` rather than a full order object.' + description: >- + Endpoint for cancelling event-market orders using the V2 response shape. + Returns `{order_id, client_order_id, reduced_by}` rather than a full + order object. x-mint: - content: | + content: > - **Rate limit:** 2 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: - orders @@ -1389,6 +1292,12 @@ paths: - $ref: '#/components/parameters/OrderIdPath' - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' - $ref: '#/components/parameters/ExchangeIndexQuery' + - name: market_ticker + in: query + description: Market ticker. Required when exchange_index is -1 (auto). + schema: + type: string + x-go-type-skip-optional-pointer: true responses: '200': description: Order cancelled successfully @@ -1402,16 +1311,25 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/events/orders/{order_id}/amend: post: operationId: AmendOrderV2 summary: Amend Order (V2) - description: 'Endpoint for amending the price and/or max fillable count of an existing event-market order using the V2 request/response shape. The request `count` is the updated total/max fillable count, equal to already filled count plus desired resting remaining count. This behavior matches the v1 amend endpoints; only the request/response shape differs.' + description: >- + Endpoint for amending the price and/or max fillable count of an existing + event-market order using the V2 request/response shape. The request + `count` is the updated total/max fillable count, equal to already filled + count plus desired resting remaining count. This behavior matches the v1 + amend endpoints; only the request/response shape differs. x-mint: - content: | + content: > - Amending a resting order preserves queue position only when the amendment decreases size. All other amendments — like increasing size or changing price forfeit queue position and place the order at the back of the queue. + + Amending a resting order preserves queue position only when the + amendment decreases size. All other amendments — like increasing size + or changing price forfeit queue position and place the order at the + back of the queue. + tags: - orders @@ -1443,12 +1361,14 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/events/orders/{order_id}/decrease: post: operationId: DecreaseOrderV2 summary: Decrease Order (V2) - description: 'Endpoint for decreasing the remaining count of an existing event-market order using the V2 request/response shape. Exactly one of `reduce_by` or `reduce_to` must be provided.' + description: >- + Endpoint for decreasing the remaining count of an existing event-market + order using the V2 request/response shape. Exactly one of `reduce_by` or + `reduce_to` must be provided. tags: - orders security: @@ -1479,7 +1399,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/order_groups: get: operationId: GetOrderGroups @@ -1506,7 +1425,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/order_groups/create: post: operationId: CreateOrderGroup @@ -1537,7 +1455,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/order_groups/{order_group_id}: get: operationId: GetOrderGroup @@ -1592,7 +1509,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/order_groups/{order_group_id}/reset: put: operationId: ResetOrderGroup @@ -1627,7 +1543,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/order_groups/{order_group_id}/trigger: put: operationId: TriggerOrderGroup @@ -1662,7 +1577,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/order_groups/{order_group_id}/limit: put: operationId: UpdateOrderGroupLimit @@ -1698,13 +1612,14 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - - # Portfolio endpoints /portfolio/balance: get: operationId: GetBalance summary: Get Balance - description: "Endpoint for getting the balance and portfolio value of a member. Both values are returned in cents. This endpoint also accepts API keys with the 'read::portfolio_balance' scope." + description: >- + Endpoint for getting the balance and portfolio value of a member. Both + values are returned in cents. This endpoint also accepts API keys with + the 'read::portfolio_balance' scope. tags: - portfolio security: @@ -1724,12 +1639,15 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts: post: operationId: CreateSubaccount summary: Create Subaccount - description: 'Creates a new subaccount for the authenticated user. This endpoint is currently only available to institutions and market makers. Subaccounts are numbered sequentially starting from 1. Maximum 63 numbered subaccounts per user (64 including the primary account).' + description: >- + Creates a new subaccount for the authenticated user. This endpoint is + currently only available to institutions and market makers. Subaccounts + are numbered sequentially starting from 1. Maximum 63 numbered + subaccounts per user (64 including the primary account). tags: - portfolio security: @@ -1749,12 +1667,13 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts/transfer: post: operationId: ApplySubaccountTransfer summary: Transfer Between Subaccounts - description: 'Transfers funds between the authenticated user''s subaccounts. Use 0 for the primary account, or 1-63 for numbered subaccounts.' + description: >- + Transfers funds between the authenticated user's subaccounts. Use 0 for + the primary account, or 1-63 for numbered subaccounts. tags: - portfolio security: @@ -1780,12 +1699,11 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts/balances: get: operationId: GetSubaccountBalances summary: Get All Subaccount Balances - description: 'Gets balances for all subaccounts including the primary account.' + description: Gets balances for all subaccounts including the primary account. tags: - portfolio security: @@ -1803,12 +1721,13 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts/transfers: get: operationId: GetSubaccountTransfers summary: Get Subaccount Transfers - description: 'Gets a paginated list of all transfers between subaccounts for the authenticated user.' + description: >- + Gets a paginated list of all transfers between subaccounts for the + authenticated user. tags: - portfolio security: @@ -1829,12 +1748,13 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts/netting: put: operationId: UpdateSubaccountNetting summary: Update Subaccount Netting - description: 'Updates the netting enabled setting for a specific subaccount. Use 0 for the primary account, or 1-63 for numbered subaccounts.' + description: >- + Updates the netting enabled setting for a specific subaccount. Use 0 for + the primary account, or 1-63 for numbered subaccounts. tags: - portfolio security: @@ -1859,7 +1779,7 @@ paths: get: operationId: GetSubaccountNetting summary: Get Subaccount Netting - description: 'Gets the netting enabled settings for all subaccounts.' + description: Gets the netting enabled settings for all subaccounts. tags: - portfolio security: @@ -1877,12 +1797,14 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/positions: get: operationId: GetPositions summary: Get Positions - description: 'Restricts the positions to those with any of following fields with non-zero values, as a comma separated list. The following values are accepted: position, total_traded' + description: >- + Restricts the positions to those with any of following fields with + non-zero values, as a comma separated list. The following values are + accepted: position, total_traded tags: - portfolio security: @@ -1909,7 +1831,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/settlements: get: operationId: GetSettlements @@ -1942,12 +1863,11 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/deposits: get: operationId: GetDeposits summary: Get Deposits - description: 'Endpoint for getting the member''s deposit history.' + description: Endpoint for getting the member's deposit history. tags: - portfolio security: @@ -1970,12 +1890,11 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/withdrawals: get: operationId: GetWithdrawals summary: Get Withdrawals - description: 'Endpoint for getting the member''s withdrawal history.' + description: Endpoint for getting the member's withdrawal history. tags: - portfolio security: @@ -1998,7 +1917,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/summary/total_resting_order_value: get: operationId: GetPortfolioRestingOrderTotalValue @@ -2016,19 +1934,24 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GetPortfolioRestingOrderTotalValueResponse' + $ref: >- + #/components/schemas/GetPortfolioRestingOrderTotalValueResponse '401': $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/fills: get: operationId: GetFills summary: Get Fills - description: | - Endpoint for getting all fills for the member. A fill is when a trade you have is matched. - Fills that occurred before the historical cutoff are only available via `GET /historical/fills`. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details. + description: > + Endpoint for getting all fills for the member. A fill is when a trade + you have is matched. + + Fills that occurred before the historical cutoff are only available via + `GET /historical/fills`. See [Historical + Data](https://docs.kalshi.com/getting_started/historical_data) for + details. tags: - portfolio security: @@ -2056,7 +1979,6 @@ paths: description: Unauthorized '500': description: Internal server error - /communications/id: get: operationId: GetCommunicationsID @@ -2079,7 +2001,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /communications/block-trade-proposals: get: operationId: GetBlockTradeProposals @@ -2096,7 +2017,9 @@ paths: - $ref: '#/components/parameters/MarketTickerQuery' - name: limit in: query - description: Parameter to specify the number of results per page. Defaults to 100. + description: >- + Parameter to specify the number of results per page. Defaults to + 100. schema: type: integer format: int32 @@ -2121,7 +2044,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - post: operationId: ProposeBlockTrade summary: Propose Block Trade @@ -2153,7 +2075,6 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' - /communications/block-trade-proposals/{block_trade_proposal_id}/accept: post: operationId: AcceptBlockTradeProposal @@ -2189,7 +2110,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /communications/rfqs: get: operationId: GetRFQs @@ -2208,7 +2128,9 @@ paths: - $ref: '#/components/parameters/SubaccountQuery' - name: limit in: query - description: Parameter to specify the number of results per page. Defaults to 100. + description: >- + Parameter to specify the number of results per page. Defaults to + 100. schema: type: integer format: int32 @@ -2233,7 +2155,7 @@ paths: $ref: '#/components/schemas/UserFilter' x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: - validate: "omitempty,oneof=self" + validate: omitempty,oneof=self responses: '200': description: RFQs retrieved successfully @@ -2245,7 +2167,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - post: operationId: CreateRFQ summary: Create RFQ @@ -2277,7 +2198,6 @@ paths: $ref: '#/components/responses/ConflictError' '500': $ref: '#/components/responses/InternalServerError' - /communications/rfqs/{rfq_id}: get: operationId: GetRFQ @@ -2304,7 +2224,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - delete: operationId: DeleteRFQ summary: Delete RFQ @@ -2326,7 +2245,98 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /communications/rfqs/{rfq_id}/quotes/{quote_id}: + delete: + operationId: DeleteRFQQuote + summary: Delete RFQ Quote + description: ' Endpoint for deleting a quote scoped to its RFQ, which means it can no longer be accepted.' + x-mint: + content: > + + **Rate limit:** 2 tokens per request. See `GET + /trade-api/v2/account/endpoint_costs` for current non-default endpoint + costs. + + + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/RfqIdPath' + - $ref: '#/components/parameters/QuoteIdPath' + responses: + '204': + description: Quote deleted successfully + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept: + put: + operationId: AcceptRFQQuote + summary: Accept RFQ Quote + description: ' Endpoint for accepting a quote scoped to its RFQ. This will require the quoter to confirm.' + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/RfqIdPath' + - $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/rfqs/{rfq_id}/quotes/{quote_id}/confirm: + put: + operationId: ConfirmRFQQuote + summary: Confirm RFQ Quote + description: ' Endpoint for confirming a quote scoped to its RFQ. This will start a timer for order execution.' + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/RfqIdPath' + - $ref: '#/components/parameters/QuoteIdPath' + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyResponse' + responses: + '204': + description: Quote confirmed successfully + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' /communications/quotes: get: operationId: GetQuotes @@ -2340,23 +2350,27 @@ paths: kalshiAccessTimestamp: [] parameters: - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/MarketTickerQuery' - name: min_ts in: query - description: Restricts the response to quotes last updated after a timestamp, formatted as a Unix Timestamp + 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 + 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. + description: >- + Parameter to specify the number of results per page. Defaults to + 500. schema: type: integer format: int32 @@ -2384,16 +2398,18 @@ paths: $ref: '#/components/schemas/UserFilter' x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: - validate: "omitempty,oneof=self" + validate: omitempty,oneof=self - name: rfq_user_filter in: query required: false - description: Filter for quotes responding to RFQs created by the authenticated user. + 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" + validate: omitempty,oneof=self - name: rfq_creator_user_id in: query description: Filter quotes by RFQ creator user ID @@ -2424,15 +2440,18 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - post: operationId: CreateQuote summary: Create Quote description: ' Endpoint for creating a quote in response to an RFQ' x-mint: - content: | + content: > - **Rate limit:** 2 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: - communications @@ -2459,12 +2478,20 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}: get: operationId: GetQuote summary: Get Quote description: ' Endpoint for getting a particular quote' + x-mint: + content: > + + + **Rate limit:** 2 tokens per request. See `GET + /trade-api/v2/account/endpoint_costs` for current non-default endpoint + costs. + + tags: - communications security: @@ -2486,15 +2513,18 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - delete: operationId: DeleteQuote summary: Delete Quote description: ' Endpoint for deleting a quote, which means it can no longer be accepted.' x-mint: - content: | + content: > - **Rate limit:** 2 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: - communications @@ -2513,7 +2543,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}/accept: put: operationId: AcceptQuote @@ -2544,7 +2573,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}/confirm: put: operationId: ConfirmQuote @@ -2573,8 +2601,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - - # Multivariate Event Collections endpoints /api_keys: get: operationId: GetApiKeys @@ -2597,7 +2623,6 @@ paths: description: Unauthorized '500': description: Internal server error - post: operationId: CreateApiKey summary: Create API Key @@ -2629,7 +2654,6 @@ paths: description: Forbidden - insufficient API usage level '500': description: Internal server error - /api_keys/generate: post: operationId: GenerateApiKey @@ -2660,7 +2684,6 @@ paths: description: Unauthorized '500': description: Internal server error - /api_keys/{api_key}: delete: operationId: DeleteApiKey @@ -2690,12 +2713,14 @@ paths: 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 API tier limits associated with the authenticated user.' + 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: @@ -2713,16 +2738,23 @@ paths: 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.' + 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: | + content: > - **Rate limit:** 30 tokens per request. See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. + + **Rate limit:** 30 tokens per request. See `GET + /trade-api/v2/account/endpoint_costs` for current non-default endpoint + costs. + tags: - account @@ -2736,17 +2768,24 @@ paths: '401': description: Unauthorized '403': - description: No API-created order was found in the user's latest 100 Predictions orders + 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. + 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.' + 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: @@ -2759,17 +2798,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GetAccountApiUsageLevelVolumeProgressResponse' + $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.' + 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: @@ -2781,15 +2822,16 @@ paths: $ref: '#/components/schemas/GetAccountEndpointCostsResponse' '500': description: Internal server error - /search/tags_by_categories: get: operationId: GetTagsForSeriesCategories summary: Get Tags for Series Categories - description: | + 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. + + This endpoint returns a mapping of series categories to their associated + tags, which can be used for filtering and search functionality. tags: - search responses: @@ -2803,15 +2845,17 @@ paths: description: Unauthorized '500': description: Internal server error - /search/filters_by_sport: get: operationId: GetFiltersForSports summary: Get Filters for Sports - description: | + 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. + + 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: @@ -2825,7 +2869,6 @@ paths: description: Unauthorized '500': description: Internal server error - /live_data/milestone/{milestone_id}: get: operationId: GetLiveDataByMilestone @@ -2844,9 +2887,11 @@ paths: in: query required: false description: >- - When true, includes player-level statistics in the live data response. - Supported for Pro Football, Pro Basketball, and College Men's Basketball milestones that have player ID mappings configured. - Has no effect for other sports or milestones without player mappings. + When true, includes player-level statistics in the live data + response. Supported for Pro Football, Pro Basketball, and College + Men's Basketball milestones that have player ID mappings configured. + Has no effect for other sports or milestones without player + mappings. schema: type: boolean default: false @@ -2861,12 +2906,14 @@ paths: description: Live data not found '500': description: Internal server error - /live_data/{type}/milestone/{milestone_id}: get: operationId: GetLiveData summary: Get Live Data (with type) - description: Get live data for a specific milestone. This is the legacy endpoint that requires a type path parameter. Prefer using `/live_data/milestone/{milestone_id}` instead. + description: >- + Get live data for a specific milestone. This is the legacy endpoint that + requires a type path parameter. Prefer using + `/live_data/milestone/{milestone_id}` instead. tags: - live-data parameters: @@ -2886,9 +2933,11 @@ paths: in: query required: false description: >- - When true, includes player-level statistics in the live data response. - Supported for Pro Football, Pro Basketball, and College Men's Basketball milestones that have player ID mappings configured. - Has no effect for other sports or milestones without player mappings. + When true, includes player-level statistics in the live data + response. Supported for Pro Football, Pro Basketball, and College + Men's Basketball milestones that have player ID mappings configured. + Has no effect for other sports or milestones without player + mappings. schema: type: boolean default: false @@ -2903,7 +2952,6 @@ paths: description: Live data not found '500': description: Internal server error - /live_data/batch: get: operationId: GetLiveDatas @@ -2927,9 +2975,11 @@ paths: in: query required: false description: >- - When true, includes player-level statistics in the live data response. - Supported for Pro Football, Pro Basketball, and College Men's Basketball milestones that have player ID mappings configured. - Has no effect for other sports or milestones without player mappings. + When true, includes player-level statistics in the live data + response. Supported for Pro Football, Pro Basketball, and College + Men's Basketball milestones that have player ID mappings configured. + Has no effect for other sports or milestones without player + mappings. schema: type: boolean default: false @@ -2942,15 +2992,16 @@ paths: $ref: '#/components/schemas/GetLiveDatasResponse' '500': description: Internal server error - /live_data/milestone/{milestone_id}/game_stats: get: operationId: GetGameStats summary: Get Game Stats description: >- - Get play-by-play game statistics for a specific milestone. - Supported sports: Pro Football, College Football, Pro Basketball, College Men's Basketball, College Women's Basketball, WNBA, Soccer, Pro Hockey, and Pro Baseball. - Returns null for unsupported milestone types or milestones without a Sportradar ID. + Get play-by-play game statistics for a specific milestone. Supported + sports: Pro Football, College Football, Pro Basketball, College Men's + Basketball, College Women's Basketball, WNBA, Soccer, Pro Hockey, and + Pro Baseball. Returns null for unsupported milestone types or milestones + without a Sportradar ID. tags: - live-data parameters: @@ -2971,8 +3022,6 @@ paths: description: Game stats not found '500': description: Internal server error - - /structured_targets: get: operationId: GetStructuredTargets @@ -2983,7 +3032,9 @@ paths: parameters: - name: ids in: query - description: Filter by specific structured target IDs. Pass multiple IDs by repeating the parameter (e.g. `?ids=uuid1&ids=uuid2`). + description: >- + Filter by specific structured target IDs. Pass multiple IDs by + repeating the parameter (e.g. `?ids=uuid1&ids=uuid2`). required: false schema: type: array @@ -3001,7 +3052,9 @@ paths: example: basketball_player - name: competition in: query - description: 'Filter by competition. Matches against the league, conference, division, or tour in the structured target details.' + description: >- + Filter by competition. Matches against the league, conference, + division, or tour in the structured target details. required: false schema: type: string @@ -3033,7 +3086,6 @@ paths: description: Unauthorized '500': description: Internal server error - /structured_targets/{structured_target_id}: get: operationId: GetStructuredTarget @@ -3061,7 +3113,6 @@ paths: description: Not found '500': description: Internal server error - /milestones/{milestone_id}: get: operationId: GetMilestone @@ -3091,7 +3142,6 @@ paths: description: Not Found '500': description: Internal Server Error - /milestones: get: operationId: GetMilestones @@ -3117,14 +3167,18 @@ paths: format: date-time - name: category in: query - description: 'Filter by milestone category. E.g. Sports, Elections, Esports, Crypto.' + description: >- + Filter by milestone category. E.g. Sports, Elections, Esports, + Crypto. required: false schema: type: string example: Sports - name: competition in: query - description: 'Filter by competition. E.g. Pro Football, Pro Basketball (M), Pro Baseball, Pro Hockey, College Football.' + description: >- + Filter by competition. E.g. Pro Football, Pro Basketball (M), Pro + Baseball, Pro Hockey, College Football. required: false schema: type: string @@ -3137,7 +3191,10 @@ paths: type: string - name: type in: query - description: 'Filter by milestone type. E.g. football_game, basketball_game, soccer_tournament_multi_leg, baseball_game, hockey_match, political_race.' + description: >- + Filter by milestone type. E.g. football_game, basketball_game, + soccer_tournament_multi_leg, baseball_game, hockey_match, + political_race. required: false schema: type: string @@ -3150,14 +3207,18 @@ paths: type: string - name: cursor in: query - description: Pagination cursor. Use the cursor value returned from the previous response to get the next page of results + description: >- + Pagination cursor. Use the cursor value returned from the previous + response to get the next page of results required: false schema: type: string - name: min_updated_ts in: query required: false - description: Filter milestones with metadata updated after this Unix timestamp (in seconds). Use this to efficiently poll for changes. + description: >- + Filter milestones with metadata updated after this Unix timestamp + (in seconds). Use this to efficiently poll for changes. schema: type: integer format: int64 @@ -3174,8 +3235,6 @@ paths: description: Unauthorized '500': description: Internal Server Error - - # Communications endpoints /multivariate_event_collections/{collection_ticker}: get: operationId: GetMultivariateEventCollection @@ -3206,7 +3265,10 @@ paths: post: operationId: CreateMarketInMultivariateEventCollection summary: Create Market In Multivariate Event Collection - description: 'Endpoint for creating an individual market in a multivariate event collection. This endpoint must be hit at least once before trading or looking up a market. Users are limited to 5000 creations per week.' + description: >- + Endpoint for creating an individual market in a multivariate event + collection. This endpoint must be hit at least once before trading or + looking up a market. Users are limited to 5000 creations per week. tags: - multivariate security: @@ -3225,14 +3287,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateMarketInMultivariateEventCollectionRequest' + $ref: >- + #/components/schemas/CreateMarketInMultivariateEventCollectionRequest responses: '200': description: Market created successfully content: application/json: schema: - $ref: '#/components/schemas/CreateMarketInMultivariateEventCollectionResponse' + $ref: >- + #/components/schemas/CreateMarketInMultivariateEventCollectionResponse '400': $ref: '#/components/responses/BadRequestError' '401': @@ -3241,7 +3305,6 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' - /multivariate_event_collections: get: operationId: GetMultivariateEventCollections @@ -3252,10 +3315,15 @@ paths: parameters: - name: status in: query - description: Only return collections of a certain status. Can be unopened, open, or closed. + description: >- + Only return collections of a certain status. Can be unopened, open, + or closed. schema: type: string - enum: [unopened, open, closed] + enum: + - unopened + - open + - closed - name: associated_event_ticker in: query description: Only return collections associated with a particular event ticker. @@ -3276,7 +3344,11 @@ paths: maximum: 200 - name: cursor in: query - description: The Cursor represents a pointer to the next page of records in the pagination. This optional parameter, when filled, should be filled with the cursor string returned in a previous request to this end-point. + description: >- + The Cursor represents a pointer to the next page of records in the + pagination. This optional parameter, when filled, should be filled + with the cursor string returned in a previous request to this + end-point. schema: type: string responses: @@ -3290,21 +3362,33 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /multivariate_event_collections/{collection_ticker}/lookup: put: operationId: LookupTickersForMarketInMultivariateEventCollection summary: Lookup Tickers For Market In Multivariate Event Collection deprecated: true - description: 'DEPRECATED: This endpoint predates RFQs and should not be used for new integrations. Endpoint for looking up an individual market in a multivariate event collection. If CreateMarketInMultivariateEventCollection has never been hit with that variable combination before, this will return a 404.' + description: >- + DEPRECATED: This endpoint predates RFQs and should not be used for new + integrations. Endpoint for looking up an individual market in a + multivariate event collection. If + CreateMarketInMultivariateEventCollection has never been hit with that + variable combination before, this will return a 404. x-mint: - content: | + content: > - This endpoint is deprecated and predates RFQs. Do not use it for new integrations. + + This endpoint is deprecated and predates RFQs. Do not use it for new + integrations. + + - **Rate limit:** 2 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: - multivariate @@ -3324,14 +3408,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LookupTickersForMarketInMultivariateEventCollectionRequest' + $ref: >- + #/components/schemas/LookupTickersForMarketInMultivariateEventCollectionRequest responses: '200': description: Market looked up successfully content: application/json: schema: - $ref: '#/components/schemas/LookupTickersForMarketInMultivariateEventCollectionResponse' + $ref: >- + #/components/schemas/LookupTickersForMarketInMultivariateEventCollectionResponse '400': $ref: '#/components/responses/BadRequestError' '401': @@ -3344,11 +3430,17 @@ paths: operationId: GetMultivariateEventCollectionLookupHistory summary: Get Multivariate Event Collection Lookup History deprecated: true - description: 'DEPRECATED: This endpoint predates RFQs and should not be used for new integrations. Endpoint for retrieving which markets in an event collection were recently looked up.' + description: >- + DEPRECATED: This endpoint predates RFQs and should not be used for new + integrations. Endpoint for retrieving which markets in an event + collection were recently looked up. x-mint: - content: | + content: > - This endpoint is deprecated and predates RFQs. Do not use it for new integrations. + + This endpoint is deprecated and predates RFQs. Do not use it for new + integrations. + tags: - multivariate @@ -3362,23 +3454,29 @@ paths: - name: lookback_seconds in: query required: true - description: Number of seconds to look back for lookup history. Must be one of 10, 60, 300, or 3600. + description: >- + Number of seconds to look back for lookup history. Must be one of + 10, 60, 300, or 3600. schema: type: integer format: int32 - enum: [10, 60, 300, 3600] + enum: + - 10 + - 60 + - 300 + - 3600 responses: '200': description: Lookup history retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMultivariateEventCollectionLookupHistoryResponse' + $ref: >- + #/components/schemas/GetMultivariateEventCollectionLookupHistoryResponse '400': $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /incentive_programs: get: operationId: GetIncentivePrograms @@ -3390,17 +3488,29 @@ paths: - name: status in: query required: false - description: 'Status filter. Can be "all", "active", "upcoming", "closed", or "paid_out". Default is "all".' + description: >- + Status filter. Can be "all", "active", "upcoming", "closed", or + "paid_out". Default is "all". schema: type: string - enum: [all, active, upcoming, closed, paid_out] + enum: + - all + - active + - upcoming + - closed + - paid_out - name: type in: query required: false - description: 'Type filter. Can be "all", "liquidity", or "volume". Default is "all".' + description: >- + Type filter. Can be "all", "liquidity", or "volume". Default is + "all". schema: type: string - enum: [all, liquidity, volume] + enum: + - all + - liquidity + - volume - name: incentive_description in: query required: false @@ -3440,14 +3550,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - /fcm/orders: get: operationId: GetFCMOrders summary: Get FCM Orders - description: | + description: > Endpoint for FCM members to get orders filtered by subtrader ID. - This endpoint requires FCM member access level and allows filtering orders by subtrader ID. + + This endpoint requires FCM member access level and allows filtering + orders by subtrader ID. tags: - fcm security: @@ -3458,7 +3569,9 @@ paths: - name: subtrader_id in: query required: true - description: Restricts the response to orders for a specific subtrader (FCM members only) + description: >- + Restricts the response to orders for a specific subtrader (FCM + members only) schema: type: string - $ref: '#/components/parameters/CursorQuery' @@ -3466,13 +3579,17 @@ paths: - $ref: '#/components/parameters/TickerQuery' - name: min_ts in: query - description: Restricts the response to orders after a timestamp, formatted as a Unix Timestamp + description: >- + Restricts the response to orders after a timestamp, formatted as a + Unix Timestamp schema: type: integer format: int64 - name: max_ts in: query - description: Restricts the response to orders before a timestamp, formatted as a Unix Timestamp + description: >- + Restricts the response to orders before a timestamp, formatted as a + Unix Timestamp schema: type: integer format: int64 @@ -3481,7 +3598,10 @@ paths: description: Restricts the response to orders that have a certain status schema: type: string - enum: [resting, canceled, executed] + enum: + - resting + - canceled + - executed - name: limit in: query description: Parameter to specify the number of results per page. Defaults to 100 @@ -3504,14 +3624,16 @@ paths: description: Not found '500': description: Internal server error - /fcm/positions: get: operationId: GetFCMPositions summary: Get FCM Positions - description: | - Endpoint for FCM members to get market positions filtered by subtrader ID. - This endpoint requires FCM member access level and allows filtering positions by subtrader ID. + description: > + Endpoint for FCM members to get market positions filtered by subtrader + ID. + + This endpoint requires FCM member access level and allows filtering + positions by subtrader ID. tags: - fcm security: @@ -3522,7 +3644,9 @@ paths: - name: subtrader_id in: query required: true - description: Restricts the response to positions for a specific subtrader (FCM members only) + description: >- + Restricts the response to positions for a specific subtrader (FCM + members only) schema: type: string - name: ticker @@ -3539,7 +3663,9 @@ paths: x-go-type-skip-optional-pointer: true - name: count_filter in: query - description: Restricts the positions to those with any of following fields with non-zero values, as a comma separated list + description: >- + Restricts the positions to those with any of following fields with + non-zero values, as a comma separated list schema: type: string - name: settlement_status @@ -3547,7 +3673,10 @@ paths: description: Settlement status of the markets to return. Defaults to unsettled schema: type: string - enum: [all, unsettled, settled] + enum: + - all + - unsettled + - settled - name: limit in: query description: Parameter to specify the number of results per page. Defaults to 100 @@ -3557,7 +3686,9 @@ paths: maximum: 1000 - name: cursor in: query - description: The Cursor represents a pointer to the next page of records in the pagination + description: >- + The Cursor represents a pointer to the next page of records in the + pagination schema: type: string responses: @@ -3575,18 +3706,27 @@ paths: description: Not found '500': description: Internal server error - /historical/cutoff: get: operationId: GetHistoricalCutoff summary: Get Historical Cutoff Timestamps - description: | - Returns the cutoff timestamps that define the boundary between **live** and **historical** data. + description: > + Returns the cutoff timestamps that define the boundary between **live** + and **historical** data. + ## Cutoff fields - - `market_settled_ts` : Markets that **settled** before this timestamp, and their candlesticks, must be accessed via `GET /historical/markets` and `GET /historical/markets/{ticker}/candlesticks`. - - `trades_created_ts` : Trades that were **filled** before this timestamp must be accessed via `GET /historical/fills`. - - `orders_updated_ts` : Orders that were **canceled or fully executed** before this timestamp must be accessed via `GET /historical/orders`. Resting (active) orders are always available in `GET /portfolio/orders`. + + - `market_settled_ts` : Markets that **settled** before this timestamp, + and their candlesticks, must be accessed via `GET /historical/markets` + and `GET /historical/markets/{ticker}/candlesticks`. + + - `trades_created_ts` : Trades that were **filled** before this + timestamp must be accessed via `GET /historical/fills`. + + - `orders_updated_ts` : Orders that were **canceled or fully executed** + before this timestamp must be accessed via `GET /historical/orders`. + Resting (active) orders are always available in `GET /portfolio/orders`. tags: - historical responses: @@ -3598,7 +3738,6 @@ paths: $ref: '#/components/schemas/GetHistoricalCutoffResponse' '500': description: Internal server error - /historical/markets/{ticker}/candlesticks: get: operationId: GetMarketCandlesticksHistorical @@ -3616,26 +3755,35 @@ paths: - name: start_ts in: query required: true - description: Start timestamp (Unix timestamp). Candlesticks will include those ending on or after this time. + description: >- + Start timestamp (Unix timestamp). Candlesticks will include those + ending on or after this time. schema: type: integer format: int64 - name: end_ts in: query required: true - description: End timestamp (Unix timestamp). Candlesticks will include those ending on or before this time. + description: >- + End timestamp (Unix timestamp). Candlesticks will include those + ending on or before this time. schema: type: integer format: int64 - name: period_interval in: query required: true - description: Time period length of each candlestick in minutes. Valid values are 1 (1 minute), 60 (1 hour), or 1440 (1 day). + description: >- + Time period length of each candlestick in minutes. Valid values are + 1 (1 minute), 60 (1 hour), or 1440 (1 day). schema: type: integer - enum: [1, 60, 1440] + enum: + - 1 + - 60 + - 1440 x-oapi-codegen-extra-tags: - validate: "required,oneof=1 60 1440" + validate: required,oneof=1 60 1440 responses: '200': description: Candlesticks retrieved successfully @@ -3649,7 +3797,6 @@ paths: description: Not found '500': description: Internal server error - /historical/fills: get: operationId: GetFillsHistorical @@ -3681,12 +3828,11 @@ paths: $ref: '#/components/responses/NotFoundError' '500': description: Internal server error - /historical/orders: get: operationId: GetHistoricalOrders summary: Get Historical Orders - description: ' Endpoint for getting orders that have been archived to the historical database.' + description: ' Endpoint for getting orders that have been archived to the historical database.' tags: - historical security: @@ -3711,7 +3857,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /historical/trades: get: operationId: GetTradesHistorical @@ -3739,13 +3884,13 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /historical/markets: get: operationId: GetHistoricalMarkets summary: Get Historical Markets - description: | - Endpoint for getting markets that have been archived to the historical database. Filters are mutually exclusive. + description: > + Endpoint for getting markets that have been archived to the historical + database. Filters are mutually exclusive. tags: - historical parameters: @@ -3766,7 +3911,6 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /historical/markets/{ticker}: get: operationId: GetHistoricalMarket @@ -3787,7 +3931,6 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - components: securitySchemes: kalshiAccessKey: @@ -3805,7 +3948,6 @@ components: in: header name: KALSHI-ACCESS-TIMESTAMP description: Request timestamp in milliseconds - responses: BadRequestError: description: Bad request - invalid input @@ -3844,12 +3986,13 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' RateLimitError: - description: 'Rate limit exceeded. The default cost is 10 tokens per request. Use GET /trade-api/v2/account/endpoint_costs to list non-default endpoint costs.' + description: >- + Rate limit exceeded. The default cost is 10 tokens per request. Use GET + /trade-api/v2/account/endpoint_costs to list non-default endpoint costs. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - parameters: LimitQuery: name: limit @@ -3862,8 +4005,7 @@ components: maximum: 1000 default: 100 x-oapi-codegen-extra-tags: - validate: "omitempty,min=1,max=1000" - + validate: omitempty,min=1,max=1000 WithdrawalLimitQuery: name: limit in: query @@ -3875,8 +4017,7 @@ components: maximum: 500 default: 100 x-oapi-codegen-extra-tags: - validate: "omitempty,min=1,max=500" - + validate: omitempty,min=1,max=500 MarketLimitQuery: name: limit in: query @@ -3889,22 +4030,22 @@ components: default: 100 x-oapi-codegen-extra-tags: validate: omitempty,gte=0,lte=1000 - CursorQuery: name: cursor in: query - description: Pagination cursor. Use the cursor value returned from the previous response to get the next page of results. Leave empty for the first page. + description: >- + Pagination cursor. Use the cursor value returned from the previous + response to get the next page of results. Leave empty for the first + page. schema: type: string x-go-type-skip-optional-pointer: true - StatusQuery: name: status in: query description: Filter by status. Possible values depend on the endpoint. schema: type: string - OrderGroupIdPath: name: order_group_id in: path @@ -3912,7 +4053,6 @@ components: description: Order group ID schema: type: string - RfqIdPath: name: rfq_id in: path @@ -3920,7 +4060,6 @@ components: description: RFQ ID schema: type: string - QuoteIdPath: name: quote_id in: path @@ -3928,7 +4067,6 @@ components: description: Quote ID schema: type: string - MarketTickerQuery: name: market_ticker in: query @@ -3936,7 +4074,6 @@ components: schema: type: string x-go-type-skip-optional-pointer: true - TickerQuery: name: ticker in: query @@ -3944,15 +4081,15 @@ components: schema: type: string x-go-type-skip-optional-pointer: true - IsBlockTradeQuery: name: is_block_trade in: query - description: | - Filter trades by whether they are block trades. Omit to return all trades. Set to `true` to return only block trades. Set to `false` to return only non-block trades. + description: > + Filter trades by whether they are block trades. Omit to return all + trades. Set to `true` to return only block trades. Set to `false` to + return only non-block trades. schema: type: boolean - SingleEventTickerQuery: name: event_ticker in: query @@ -3960,7 +4097,6 @@ components: schema: type: string x-go-type-skip-optional-pointer: true - MultipleEventTickerQuery: name: event_ticker in: query @@ -3968,14 +4104,15 @@ components: schema: type: string x-go-type-skip-optional-pointer: true - PositionsCursorQuery: name: cursor in: query - description: The Cursor represents a pointer to the next page of records in the pagination. Use the value returned from the previous response to get the next page. + description: >- + The Cursor represents a pointer to the next page of records in the + pagination. Use the value returned from the previous response to get the + next page. schema: type: string - PositionsLimitQuery: name: limit in: query @@ -3986,14 +4123,15 @@ components: minimum: 1 maximum: 1000 default: 100 - CountFilterQuery: name: count_filter in: query - description: Restricts the positions to those with any of following fields with non-zero values, as a comma separated list. The following values are accepted - position, total_traded + description: >- + Restricts the positions to those with any of following fields with + non-zero values, as a comma separated list. The following values are + accepted - position, total_traded schema: type: string - OrderIdQuery: name: order_id in: query @@ -4001,7 +4139,6 @@ components: schema: type: string x-go-type-skip-optional-pointer: true - MinTsQuery: name: min_ts in: query @@ -4009,7 +4146,6 @@ components: schema: type: integer format: int64 - MaxTsQuery: name: max_ts in: query @@ -4017,28 +4153,26 @@ components: schema: type: integer format: int64 - SubaccountQuery: name: subaccount in: query - description: Subaccount number (0 for primary, 1-63 for subaccounts). If omitted, defaults to all subaccounts. + description: >- + Subaccount number (0 for primary, 1-63 for subaccounts). If omitted, + defaults to all subaccounts. schema: type: integer - SubaccountQueryDefaultPrimary: name: subaccount in: query description: Subaccount number (0 for primary, 1-63 for subaccounts). Defaults to 0. schema: type: integer - ExchangeIndexQuery: name: exchange_index in: query schema: $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true - OrderIdPath: name: order_id in: path @@ -4046,7 +4180,6 @@ components: description: Order ID schema: type: string - TickerPath: name: ticker in: path @@ -4054,7 +4187,6 @@ components: description: Market ticker schema: type: string - SeriesTickerQuery: name: series_ticker in: query @@ -4062,7 +4194,6 @@ components: schema: type: string x-go-type-skip-optional-pointer: true - MinCreatedTsQuery: name: min_created_ts in: query @@ -4070,7 +4201,6 @@ components: schema: type: integer format: int64 - MaxCreatedTsQuery: name: max_created_ts in: query @@ -4078,15 +4208,17 @@ components: schema: type: integer format: int64 - MinUpdatedTsQuery: name: min_updated_ts in: query - description: Return markets with metadata updated later than this Unix timestamp. Tracks non-trading changes only. Incompatible with any other filters except mve_filter=exclude. May be combined with series_ticker, which requires mve_filter=exclude. + description: >- + Return markets with metadata updated later than this Unix timestamp. + Tracks non-trading changes only. Incompatible with any other filters + except mve_filter=exclude. May be combined with series_ticker, which + requires mve_filter=exclude. schema: type: integer format: int64 - MaxCloseTsQuery: name: max_close_ts in: query @@ -4094,7 +4226,6 @@ components: schema: type: integer format: int64 - MinCloseTsQuery: name: min_close_ts in: query @@ -4102,7 +4233,6 @@ components: schema: type: integer format: int64 - MinSettledTsQuery: name: min_settled_ts in: query @@ -4110,7 +4240,6 @@ components: schema: type: integer format: int64 - MaxSettledTsQuery: name: max_settled_ts in: query @@ -4118,73 +4247,94 @@ components: schema: type: integer format: int64 - MarketStatusQuery: name: status in: query description: Filter by market status. Leave empty to return markets with any status. schema: type: string - enum: [unopened, open, paused, closed, settled] - + enum: + - unopened + - open + - paused + - closed + - settled TickersQuery: name: tickers in: query - description: Filter by specific market tickers. Comma-separated list of market tickers to retrieve. + description: >- + Filter by specific market tickers. Comma-separated list of market + tickers to retrieve. schema: type: string - EventTickersQuery: name: tickers in: query - description: Filter by specific event tickers. Comma-separated list of event tickers to retrieve. + description: >- + Filter by specific event tickers. Comma-separated list of event tickers + to retrieve. schema: type: string - MveFilterQuery: name: mve_filter in: query - description: Filter by multivariate events (combos). 'only' returns only multivariate events, 'exclude' excludes multivariate events. + description: >- + Filter by multivariate events (combos). 'only' returns only multivariate + events, 'exclude' excludes multivariate events. schema: type: string - enum: ['only', 'exclude'] - + enum: + - only + - exclude MveHistoricalFilterQuery: name: mve_filter in: query - description: Filter by multivariate events (combos). By default, MVE markets are included. + description: >- + Filter by multivariate events (combos). By default, MVE markets are + included. schema: type: string - enum: ['exclude'] + enum: + - exclude nullable: true default: null - schemas: - # Common schemas FixedPointDollars: type: string - description: US dollar amount as a fixed-point decimal string with up to 6 decimal places of precision. This is the maximum supported precision; valid quote intervals for a given market are constrained by that market's price level structure. - example: "0.5600" - + description: >- + US dollar amount as a fixed-point decimal string with up to 6 decimal + places of precision. This is the maximum supported precision; valid + quote intervals for a given market are constrained by that market's + price level structure. + example: '0.5600' FixedPointCount: type: string - description: Fixed-point contract count string (2 decimals, e.g., "10.00"; referred to as "fp" in field names). Requests accept 0–2 decimal places (e.g., "10", "10.0", "10.00"); responses always emit 2 decimals. Fractional contract values (e.g., "2.50") are supported on markets with fractional trading enabled; the minimum granularity is 0.01 contracts. Integer contract count fields are legacy and will be deprecated; when both integer and fp fields are provided, they must match. - example: "10.00" - + description: >- + Fixed-point contract count string (2 decimals, e.g., "10.00"; referred + to as "fp" in field names). Requests accept 0–2 decimal places (e.g., + "10", "10.0", "10.00"); responses always emit 2 decimals. Fractional + contract values (e.g., "2.50") are supported on markets with fractional + trading enabled; the minimum granularity is 0.01 contracts. Integer + contract count fields are legacy and will be deprecated; when both + integer and fp fields are provided, they must match. + example: '10.00' ExchangeIndex: type: integer - description: "Identifier for an exchange shard. Defaults to 0 if unspecified. Note: currently only 0 supported." + description: >- + Identifier for an exchange shard. Defaults to 0 if unspecified. Note: + currently only 0 supported. example: 0 - FeeType: type: string - enum: [quadratic, quadratic_with_maker_fees, flat] + enum: + - quadratic + - quadratic_with_maker_fees + - flat x-enum-varnames: - FeeTypeQuadratic - FeeTypeQuadraticWithMakerFees - FeeTypeFlat description: Fee type for a series or scheduled fee override. - GetMarketCandlesticksHistoricalResponse: type: object required: @@ -4199,7 +4349,6 @@ components: description: Array of candlestick data points for the specified time range. items: $ref: '#/components/schemas/MarketCandlestickHistorical' - MarketCandlestickHistorical: type: object required: @@ -4216,20 +4365,29 @@ components: description: Unix timestamp for the inclusive end of the candlestick period. yes_bid: $ref: '#/components/schemas/BidAskDistributionHistorical' - description: Open, high, low, close (OHLC) data for YES buy offers on the market during the candlestick period. + description: >- + Open, high, low, close (OHLC) data for YES buy offers on the market + during the candlestick period. yes_ask: $ref: '#/components/schemas/BidAskDistributionHistorical' - description: Open, high, low, close (OHLC) data for YES sell offers on the market during the candlestick period. + description: >- + Open, high, low, close (OHLC) data for YES sell offers on the market + during the candlestick period. price: $ref: '#/components/schemas/PriceDistributionHistorical' - description: Open, high, low, close (OHLC) and more data for trade YES contract prices on the market during the candlestick period. + description: >- + Open, high, low, close (OHLC) and more data for trade YES contract + prices on the market during the candlestick period. volume: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought on the market during the candlestick period. + description: >- + String representation of the number of contracts bought on the + market during the candlestick period. open_interest: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought on the market by end of the candlestick period (end_period_ts). - + description: >- + String representation of the number of contracts bought on the + market by end of the candlestick period (end_period_ts). BidAskDistributionHistorical: type: object required: @@ -4240,17 +4398,24 @@ components: properties: open: $ref: '#/components/schemas/FixedPointDollars' - description: Offer price on the market at the start of the candlestick period (in dollars). + description: >- + Offer price on the market at the start of the candlestick period (in + dollars). low: $ref: '#/components/schemas/FixedPointDollars' - description: Lowest offer price on the market during the candlestick period (in dollars). + description: >- + Lowest offer price on the market during the candlestick period (in + dollars). high: $ref: '#/components/schemas/FixedPointDollars' - description: Highest offer price on the market during the candlestick period (in dollars). + description: >- + Highest offer price on the market during the candlestick period (in + dollars). close: $ref: '#/components/schemas/FixedPointDollars' - description: Offer price on the market at the end of the candlestick period (in dollars). - + description: >- + Offer price on the market at the end of the candlestick period (in + dollars). PriceDistributionHistorical: type: object required: @@ -4265,33 +4430,44 @@ components: allOf: - $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Price of the first trade during the candlestick period (in dollars). Null if no trades occurred. + description: >- + Price of the first trade during the candlestick period (in dollars). + Null if no trades occurred. low: allOf: - $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Lowest trade price during the candlestick period (in dollars). Null if no trades occurred. + description: >- + Lowest trade price during the candlestick period (in dollars). Null + if no trades occurred. high: allOf: - $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Highest trade price during the candlestick period (in dollars). Null if no trades occurred. + description: >- + Highest trade price during the candlestick period (in dollars). Null + if no trades occurred. close: allOf: - $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Price of the last trade during the candlestick period (in dollars). Null if no trades occurred. + description: >- + Price of the last trade during the candlestick period (in dollars). + Null if no trades occurred. mean: allOf: - $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Volume-weighted average price during the candlestick period (in dollars). Null if no trades occurred. + description: >- + Volume-weighted average price during the candlestick period (in + dollars). Null if no trades occurred. previous: allOf: - $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Close price from the previous candlestick period (in dollars). Null if this is the first candlestick or no prior trade exists. - + description: >- + Close price from the previous candlestick period (in dollars). Null + if this is the first candlestick or no prior trade exists. ErrorResponse: type: object properties: @@ -4307,40 +4483,72 @@ components: service: type: string description: The name of the service that generated the error - SelfTradePreventionType: type: string - enum: ['taker_at_cross', 'maker'] - description: | - The self-trade prevention type for orders. `taker_at_cross` cancels the taker order when it would trade against another order from the same user; execution stops and any partial fills already matched are executed. `maker` cancels the resting maker order and continues matching. - + enum: + - taker_at_cross + - maker + description: > + The self-trade prevention type for orders. `taker_at_cross` cancels the + taker order when it would trade against another order from the same + user; execution stops and any partial fills already matched are + executed. `maker` cancels the resting maker order and continues + matching. BookSide: type: string - enum: ['bid', 'ask'] - description: 'Side of the book for an order or trade. For event markets, this refers to the YES leg only: `bid` means buy YES, `ask` means sell YES. (Selling YES is economically equivalent to buying NO at `1 - price`, but this endpoint quotes everything from the YES side.)' - + enum: + - bid + - ask + description: >- + Side of the book for an order or trade. For event markets, this refers + to the YES leg only: `bid` means buy YES, `ask` means sell YES. (Selling + YES is economically equivalent to buying NO at `1 - price`, but this + endpoint quotes everything from the YES side.) OrderStatus: type: string - enum: ['resting', 'canceled', 'executed'] + enum: + - resting + - canceled + - executed description: The status of an order - ExchangeInstance: type: string - enum: ['event_contract', 'margined'] + enum: + - event_contract + - margined description: The exchange instance type - UserFilter: type: string - enum: ['self'] - x-enum-varnames: ['UserFilterSelf'] - description: Omit or leave empty to return all results. Use `self` to filter by the authenticated user. - + enum: + - self + x-enum-varnames: + - UserFilterSelf + description: >- + Omit or leave empty to return all results. Use `self` to filter by the + authenticated user. ApiKeyScope: type: string - enum: ['read', 'write', 'read::block_trade_accept', 'read::portfolio_balance', 'write::transfer', 'write::block_trade_accept'] - x-enum-varnames: ['ApiKeyScopeRead', 'ApiKeyScopeWrite', 'ApiKeyScopeReadBlockTradeAccept', 'ApiKeyScopeReadPortfolioBalance', 'ApiKeyScopeWriteTransfer', 'ApiKeyScopeWriteBlockTradeAccept'] - description: Scope granted to an API key. Parent scopes grant broad access; for example, `read` grants all read endpoints and `write` grants all write endpoints. Child scopes such as `read::block_trade_accept`, `read::portfolio_balance`, `write::transfer`, and `write::block_trade_accept` grant only their specific endpoint group and can be granted without the parent scope. - + enum: + - read + - write + - read::block_trade_accept + - read::portfolio_balance + - write::transfer + - write::block_trade_accept + x-enum-varnames: + - ApiKeyScopeRead + - ApiKeyScopeWrite + - ApiKeyScopeReadBlockTradeAccept + - ApiKeyScopeReadPortfolioBalance + - ApiKeyScopeWriteTransfer + - ApiKeyScopeWriteBlockTradeAccept + description: >- + Scope granted to an API key. Parent scopes grant broad access; for + example, `read` grants all read endpoints and `write` grants all write + endpoints. Child scopes such as `read::block_trade_accept`, + `read::portfolio_balance`, `write::transfer`, and + `write::block_trade_accept` grant only their specific endpoint group and + can be granted without the parent scope. ApiKey: type: object required: @@ -4359,7 +4567,6 @@ components: description: List of scopes granted to this API key. items: $ref: '#/components/schemas/ApiKeyScope' - GetApiKeysResponse: type: object required: @@ -4370,7 +4577,6 @@ components: description: List of all API keys associated with the user items: $ref: '#/components/schemas/ApiKey' - CreateApiKeyRequest: type: object required: @@ -4382,13 +4588,18 @@ components: description: Name for the API key. This helps identify the key's purpose public_key: type: string - description: RSA public key in PEM format. This will be used to verify signatures on API requests + description: >- + RSA public key in PEM format. This will be used to verify signatures + on API requests scopes: type: array - description: List of scopes to grant to the API key. If the broad `write` parent scope is included, `read` must also be included. Child scopes may be granted without the broad parent scope. Defaults to full access (`read`, `write`) if not provided. + description: >- + List of scopes to grant to the API key. If the broad `write` parent + scope is included, `read` must also be included. Child scopes may be + granted without the broad parent scope. Defaults to full access + (`read`, `write`) if not provided. items: $ref: '#/components/schemas/ApiKeyScope' - CreateApiKeyResponse: type: object required: @@ -4397,7 +4608,6 @@ components: api_key_id: type: string description: Unique identifier for the newly created API key - GenerateApiKeyRequest: type: object required: @@ -4408,10 +4618,13 @@ components: description: Name for the API key. This helps identify the key's purpose scopes: type: array - description: List of scopes to grant to the API key. If the broad `write` parent scope is included, `read` must also be included. Child scopes may be granted without the broad parent scope. Defaults to full access (`read`, `write`) if not provided. + description: >- + List of scopes to grant to the API key. If the broad `write` parent + scope is included, `read` must also be included. Child scopes may be + granted without the broad parent scope. Defaults to full access + (`read`, `write`) if not provided. items: $ref: '#/components/schemas/ApiKeyScope' - GenerateApiKeyResponse: type: object required: @@ -4423,8 +4636,9 @@ components: description: Unique identifier for the newly generated API key private_key: type: string - description: RSA private key in PEM format. This must be stored securely and cannot be retrieved again after this response - + description: >- + RSA private key in PEM format. This must be stored securely and + cannot be retrieved again after this response GetTagsForSeriesCategoriesResponse: type: object required: @@ -4437,7 +4651,6 @@ components: type: array items: type: string - ScopeList: type: object required: @@ -4448,7 +4661,6 @@ components: description: List of scopes items: type: string - SportFilterDetails: type: object required: @@ -4465,7 +4677,6 @@ components: description: Mapping of competitions to their scope lists additionalProperties: $ref: '#/components/schemas/ScopeList' - GetFiltersBySportsResponse: type: object required: @@ -4482,7 +4693,6 @@ components: description: Ordered list of sports for display items: type: string - BucketLimit: type: object description: | @@ -4506,7 +4716,6 @@ components: headroom that idle clients accumulate and can spend in a single pulse (e.g. write buckets at non-Basic tiers hold two seconds of budget). - GetAccountApiLimitsResponse: type: object required: @@ -4517,17 +4726,23 @@ components: properties: usage_tier: type: string - description: User's API usage tier. + description: >- + User's effective Predictions API usage tier for these limits (for + example, basic, advanced, expert, premier, paragon, prime, or + prestige). + example: expert read: $ref: '#/components/schemas/BucketLimit' write: $ref: '#/components/schemas/BucketLimit' grants: type: array - description: The caller's active API usage level grants across exchange lanes, where each grant applies to its exchange_instance and usage_tier reflects the effective tier for the lane reported by this endpoint. + description: >- + The caller's active API usage level grants across exchange lanes, + where each grant applies to its exchange_instance and usage_tier + reflects the effective tier for the lane reported by this endpoint. items: $ref: '#/components/schemas/ApiUsageLevelGrant' - ApiUsageLevelGrant: type: object required: @@ -4539,16 +4754,22 @@ components: $ref: '#/components/schemas/ExchangeInstance' level: type: string - description: API usage level this grant confers (e.g. premier, paragon, prime). + description: >- + API usage level this grant confers (for example, expert, premier, + paragon, prime, or prestige). + example: prestige expires_ts: type: integer format: int64 nullable: true - description: Unix timestamp (seconds) when the grant expires. Absent for permanent grants. + description: >- + Unix timestamp (seconds) when the grant expires. Absent for + permanent grants. source: type: string - description: 'How the grant was created: "volume" (earned from trading volume) or "manual" (assigned by Kalshi).' - + description: >- + How the grant was created: "volume" (earned from trading volume) or + "manual" (assigned by Kalshi). GetAccountApiUsageLevelVolumeProgressResponse: type: object required: @@ -4556,10 +4777,12 @@ components: properties: volume_progress: type: array - description: Latest cron-computed trading volume progress toward volume-based API usage tiers for the predictions (event_contract) lane. + description: >- + Latest cron-computed trading volume progress toward volume-based API + usage tiers for the predictions (event_contract) lane. Volume-based + public tiers are Expert, Premier, Paragon, Prime, and Prestige. items: $ref: '#/components/schemas/AccountApiUsageLevelVolumeProgress' - AccountApiUsageLevelVolumeProgress: type: object required: @@ -4570,14 +4793,16 @@ components: computed_ts: type: integer format: int64 - description: 'Unix timestamp (seconds) when this progress was computed; trailing_30d_volume_fp covers the trailing 30 days ending at this time.' + description: >- + Unix timestamp (seconds) when this progress was computed; + trailing_30d_volume_fp covers the trailing 30 days ending at this + time. trailing_30d_volume_fp: $ref: '#/components/schemas/FixedPointCount' goals: type: array items: $ref: '#/components/schemas/AccountApiUsageLevelVolumeGoal' - AccountApiUsageLevelVolumeGoal: type: object required: @@ -4587,12 +4812,12 @@ components: properties: level: type: string - description: API usage level for this volume goal. + description: API usage level for this Predictions volume goal. + example: expert earn_volume_goal_fp: $ref: '#/components/schemas/FixedPointCount' keep_volume_goal_fp: $ref: '#/components/schemas/FixedPointCount' - EndpointTokenCost: type: object required: @@ -4608,8 +4833,9 @@ components: description: API route path for the endpoint. cost: type: integer - description: Configured token cost for an endpoint whose cost differs from the default cost. - + description: >- + Configured token cost for an endpoint whose cost differs from the + default cost. GetAccountEndpointCostsResponse: type: object required: @@ -4618,13 +4844,16 @@ components: properties: default_cost: type: integer - description: Default token cost applied to endpoints that are not listed in `endpoint_costs`. This is currently 10. + description: >- + Default token cost applied to endpoints that are not listed in + `endpoint_costs`. This is currently 10. endpoint_costs: type: array - description: API v2 endpoints whose configured token cost differs from `default_cost`. Endpoints that use the default cost are omitted. + description: >- + API v2 endpoints whose configured token cost differs from + `default_cost`. Endpoints that use the default cost are omitted. items: $ref: '#/components/schemas/EndpointTokenCost' - ExchangeStatus: type: object required: @@ -4633,16 +4862,23 @@ components: properties: exchange_active: type: boolean - description: False if the core Kalshi exchange is no longer taking any state changes at all. This includes but is not limited to trading, new users, and transfers. True unless we are under maintenance. + description: >- + False if the core Kalshi exchange is no longer taking any state + changes at all. This includes but is not limited to trading, new + users, and transfers. True unless we are under maintenance. trading_active: type: boolean - description: True if we are currently permitting trading on the exchange. This is true during trading hours and false outside exchange hours. Kalshi reserves the right to pause at any time in case issues are detected. + description: >- + True if we are currently permitting trading on the exchange. This is + true during trading hours and false outside exchange hours. Kalshi + reserves the right to pause at any time in case issues are detected. exchange_estimated_resume_time: type: string format: date-time - description: Estimated downtime for the current exchange maintenance window. However, this is not guaranteed and can be extended. + description: >- + Estimated downtime for the current exchange maintenance window. + However, this is not guaranteed and can be extended. nullable: true - GetExchangeAnnouncementsResponse: type: object required: @@ -4653,7 +4889,6 @@ components: description: A list of exchange-wide announcements. items: $ref: '#/components/schemas/Announcement' - Announcement: type: object required: @@ -4664,7 +4899,10 @@ components: properties: type: type: string - enum: [info, warning, error] + enum: + - info + - warning + - error description: The type of the announcement. message: type: string @@ -4675,9 +4913,10 @@ components: description: The time the announcement was delivered. status: type: string - enum: [active, inactive] + enum: + - active + - inactive description: The current status of this announcement. - GetExchangeScheduleResponse: type: object required: @@ -4685,7 +4924,6 @@ components: properties: schedule: $ref: '#/components/schemas/Schedule' - Schedule: type: object required: @@ -4694,15 +4932,18 @@ components: properties: standard_hours: type: array - description: The standard operating hours of the exchange. All times are expressed in ET. Outside of these times trading will be unavailable. + description: >- + The standard operating hours of the exchange. All times are + expressed in ET. Outside of these times trading will be unavailable. items: $ref: '#/components/schemas/WeeklySchedule' maintenance_windows: type: array - description: Scheduled maintenance windows, during which the exchange may be unavailable. + description: >- + Scheduled maintenance windows, during which the exchange may be + unavailable. items: $ref: '#/components/schemas/MaintenanceWindow' - WeeklySchedule: type: object required: @@ -4723,7 +4964,9 @@ components: end_time: type: string format: date-time - description: End date and time for when this weekly schedule is no longer effective. + description: >- + End date and time for when this weekly schedule is no longer + effective. monday: type: array description: Trading hours for Monday. May contain multiple sessions. @@ -4759,7 +5002,6 @@ components: description: Trading hours for Sunday. May contain multiple sessions. items: $ref: '#/components/schemas/DailySchedule' - DailySchedule: type: object required: @@ -4772,7 +5014,6 @@ components: close_time: type: string description: Closing time in ET (Eastern Time) format HH:MM. - MaintenanceWindow: type: object required: @@ -4787,7 +5028,6 @@ components: type: string format: date-time description: End date and time of the maintenance window. - GetHistoricalCutoffResponse: type: object required: @@ -4798,19 +5038,25 @@ components: market_settled_ts: type: string format: date-time - description: | - Cutoff based on **market settlement time**. Markets and their candlesticks that settled before this timestamp must be accessed via `GET /historical/markets` and `GET /historical/markets/{ticker}/candlesticks`. + description: > + Cutoff based on **market settlement time**. Markets and their + candlesticks that settled before this timestamp must be accessed via + `GET /historical/markets` and `GET + /historical/markets/{ticker}/candlesticks`. trades_created_ts: type: string format: date-time - description: | - Cutoff based on **trade fill time**. Fills that occurred before this timestamp must be accessed via `GET /historical/fills`. + description: > + Cutoff based on **trade fill time**. Fills that occurred before this + timestamp must be accessed via `GET /historical/fills`. orders_updated_ts: type: string format: date-time - description: | - Cutoff based on **order cancellation or execution time**. Orders canceled or fully executed before this timestamp must be accessed via `GET /historical/orders`. Resting (active) orders are always available in `GET /portfolio/orders`. - + description: > + Cutoff based on **order cancellation or execution time**. Orders + canceled or fully executed before this timestamp must be accessed + via `GET /historical/orders`. Resting (active) orders are always + available in `GET /portfolio/orders`. GetUserDataTimestampResponse: type: object required: @@ -4820,7 +5066,6 @@ components: type: string format: date-time description: Timestamp when user data was last updated. - GetMarketCandlesticksResponse: type: object required: @@ -4835,7 +5080,6 @@ components: description: Array of candlestick data points for the specified time range. items: $ref: '#/components/schemas/MarketCandlestick' - GetEventCandlesticksResponse: type: object required: @@ -4850,7 +5094,9 @@ components: type: string market_candlesticks: type: array - description: Array of market candlestick arrays, one for each market in the event. + description: >- + Array of market candlestick arrays, one for each market in the + event. items: type: array items: @@ -4858,8 +5104,9 @@ components: adjusted_end_ts: type: integer format: int64 - description: Adjusted end timestamp if the requested candlesticks would be larger than maxAggregateCandidates. - + description: >- + Adjusted end timestamp if the requested candlesticks would be larger + than maxAggregateCandidates. BatchGetMarketCandlesticksResponse: type: object required: @@ -4870,7 +5117,6 @@ components: description: Array of market candlestick data, one entry per requested market. items: $ref: '#/components/schemas/MarketCandlesticksResponse' - MarketCandlesticksResponse: type: object required: @@ -4882,10 +5128,11 @@ components: description: Market ticker string (e.g., 'INXD-24JAN01'). candlesticks: type: array - description: Array of candlestick data points for the market. Includes an initial data point at the start timestamp when available. + description: >- + Array of candlestick data points for the market. Includes an initial + data point at the start timestamp when available. items: $ref: '#/components/schemas/MarketCandlestick' - MarketCandlestick: type: object required: @@ -4902,20 +5149,29 @@ components: description: Unix timestamp for the inclusive end of the candlestick period. yes_bid: $ref: '#/components/schemas/BidAskDistribution' - description: Open, high, low, close (OHLC) data for YES buy offers on the market during the candlestick period. + description: >- + Open, high, low, close (OHLC) data for YES buy offers on the market + during the candlestick period. yes_ask: $ref: '#/components/schemas/BidAskDistribution' - description: Open, high, low, close (OHLC) data for YES sell offers on the market during the candlestick period. + description: >- + Open, high, low, close (OHLC) data for YES sell offers on the market + during the candlestick period. price: $ref: '#/components/schemas/PriceDistribution' - description: Open, high, low, close (OHLC) and more data for trade YES contract prices on the market during the candlestick period. + description: >- + Open, high, low, close (OHLC) and more data for trade YES contract + prices on the market during the candlestick period. volume_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought on the market during the candlestick period. + description: >- + String representation of the number of contracts bought on the + market during the candlestick period. open_interest_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought on the market by end of the candlestick period (end_period_ts). - + description: >- + String representation of the number of contracts bought on the + market by end of the candlestick period (end_period_ts). BidAskDistribution: type: object required: @@ -4926,54 +5182,81 @@ components: properties: open_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Offer price on the market at the start of the candlestick period (in dollars). + description: >- + Offer price on the market at the start of the candlestick period (in + dollars). low_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Lowest offer price on the market during the candlestick period (in dollars). + description: >- + Lowest offer price on the market during the candlestick period (in + dollars). high_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Highest offer price on the market during the candlestick period (in dollars). + description: >- + Highest offer price on the market during the candlestick period (in + dollars). close_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Offer price on the market at the end of the candlestick period (in dollars). - + description: >- + Offer price on the market at the end of the candlestick period (in + dollars). PriceDistribution: type: object properties: open_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: First traded YES contract price on the market during the candlestick period (in dollars). May be null if there was no trade during the period. + description: >- + First traded YES contract price on the market during the candlestick + period (in dollars). May be null if there was no trade during the + period. low_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Lowest traded YES contract price on the market during the candlestick period (in dollars). May be null if there was no trade during the period. + description: >- + Lowest traded YES contract price on the market during the + candlestick period (in dollars). May be null if there was no trade + during the period. high_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Highest traded YES contract price on the market during the candlestick period (in dollars). May be null if there was no trade during the period. + description: >- + Highest traded YES contract price on the market during the + candlestick period (in dollars). May be null if there was no trade + during the period. close_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Last traded YES contract price on the market during the candlestick period (in dollars). May be null if there was no trade during the period. + description: >- + Last traded YES contract price on the market during the candlestick + period (in dollars). May be null if there was no trade during the + period. mean_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Mean traded YES contract price on the market during the candlestick period (in dollars). May be null if there was no trade during the period. + description: >- + Mean traded YES contract price on the market during the candlestick + period (in dollars). May be null if there was no trade during the + period. previous_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Last traded YES contract price on the market before the candlestick period (in dollars). May be null if there were no trades before the period. + description: >- + Last traded YES contract price on the market before the candlestick + period (in dollars). May be null if there were no trades before the + period. min_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Minimum close price of any market during the candlestick period (in dollars). + description: >- + Minimum close price of any market during the candlestick period (in + dollars). max_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: Maximum close price of any market during the candlestick period (in dollars). - - # Live Data schemas + description: >- + Maximum close price of any market during the candlestick period (in + dollars). LiveData: type: object required: @@ -4991,7 +5274,6 @@ components: milestone_id: type: string description: Milestone ID - GetLiveDataResponse: type: object required: @@ -4999,7 +5281,6 @@ components: properties: live_data: $ref: '#/components/schemas/LiveData' - GetLiveDatasResponse: type: object required: @@ -5009,13 +5290,11 @@ components: type: array items: $ref: '#/components/schemas/LiveData' - GetGameStatsResponse: type: object properties: pbp: $ref: '#/components/schemas/PlayByPlay' - PlayByPlay: type: object description: Play-by-play data organized by period. @@ -5030,7 +5309,6 @@ components: items: type: object additionalProperties: true - IndexedBalance: type: object required: @@ -5041,7 +5319,6 @@ components: $ref: '#/components/schemas/ExchangeIndex' balance: $ref: '#/components/schemas/FixedPointDollars' - GetBalanceResponse: type: object required: @@ -5053,14 +5330,20 @@ components: balance: type: integer format: int64 - description: Member's available balance in cents. This represents the amount available for trading. + description: >- + Member's available balance in cents. This represents the amount + available for trading. balance_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Member's available balance as a fixed-point dollar string. This represents the amount available for trading. + description: >- + Member's available balance as a fixed-point dollar string. This + represents the amount available for trading. portfolio_value: type: integer format: int64 - description: Member's portfolio value in cents. This is the current value of all positions held. + description: >- + Member's portfolio value in cents. This is the current value of all + positions held. updated_ts: type: integer format: int64 @@ -5070,7 +5353,6 @@ components: items: $ref: '#/components/schemas/IndexedBalance' description: Balance broken down per exchange index. - CreateSubaccountResponse: type: object required: @@ -5079,7 +5361,6 @@ components: subaccount_number: type: integer description: The sequential number assigned to this subaccount (1-63). - ApplySubaccountTransferRequest: type: object required: @@ -5093,22 +5374,24 @@ components: format: uuid description: Unique client-provided transfer ID for idempotency. x-oapi-codegen-extra-tags: - validate: "required" + validate: required from_subaccount: type: integer - description: Source subaccount number (0 for primary, 1-63 for numbered subaccounts). + description: >- + Source subaccount number (0 for primary, 1-63 for numbered + subaccounts). to_subaccount: type: integer - description: Destination subaccount number (0 for primary, 1-63 for numbered subaccounts). + description: >- + Destination subaccount number (0 for primary, 1-63 for numbered + subaccounts). amount_cents: type: integer format: int64 description: Amount to transfer in cents. - ApplySubaccountTransferResponse: type: object description: Empty response indicating successful transfer. - GetSubaccountBalancesResponse: type: object required: @@ -5118,17 +5401,20 @@ components: type: array items: $ref: '#/components/schemas/SubaccountBalance' - SubaccountBalance: type: object required: - subaccount_number + - exchange_index - balance - updated_ts properties: subaccount_number: type: integer description: Subaccount number (0 for primary, 1-63 for subaccounts). + exchange_index: + type: integer + description: Exchange index the balance is held on. balance: $ref: '#/components/schemas/FixedPointDollars' description: Balance in dollars. @@ -5136,7 +5422,6 @@ components: type: integer format: int64 description: Unix timestamp of last balance update. - GetSubaccountTransfersResponse: type: object required: @@ -5149,7 +5434,6 @@ components: cursor: type: string description: Cursor for the next page of results. - SubaccountTransfer: type: object required: @@ -5176,7 +5460,6 @@ components: type: integer format: int64 description: Unix timestamp when the transfer was created. - UpdateSubaccountNettingRequest: type: object required: @@ -5189,7 +5472,6 @@ components: enabled: type: boolean description: Whether netting is enabled for this subaccount. - GetSubaccountNettingResponse: type: object required: @@ -5199,7 +5481,6 @@ components: type: array items: $ref: '#/components/schemas/SubaccountNettingConfig' - SubaccountNettingConfig: type: object required: @@ -5212,8 +5493,6 @@ components: enabled: type: boolean description: Whether netting is enabled for this subaccount. - - # Portfolio schemas (specific to portfolio endpoints, not shared with IB) GetSettlementsResponse: type: object required: @@ -5225,7 +5504,6 @@ components: $ref: '#/components/schemas/Settlement' cursor: type: string - Settlement: type: object required: @@ -5248,36 +5526,47 @@ components: description: The event ticker symbol of the market that was settled. market_result: type: string - enum: ['yes', 'no', 'scalar'] - description: The outcome of the market settlement. 'yes' = market resolved to YES, 'no' = market resolved to NO, 'scalar' = scalar market settled at a specific value. + enum: + - 'yes' + - 'no' + - scalar + description: >- + The outcome of the market settlement. 'yes' = market resolved to + YES, 'no' = market resolved to NO, 'scalar' = scalar market settled + at a specific value. yes_count_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of YES contracts owned at the time of settlement. + description: >- + String representation of the number of YES contracts owned at the + time of settlement. yes_total_cost_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Total cost basis of all YES contracts in fixed-point dollars. no_count_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of NO contracts owned at the time of settlement. + description: >- + String representation of the number of NO contracts owned at the + time of settlement. no_total_cost_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Total cost basis of all NO contracts in fixed-point dollars. revenue: type: integer - description: Total revenue earned from this settlement in cents (winning contracts pay out 100 cents each). + description: >- + Total revenue earned from this settlement in cents (winning + contracts pay out 100 cents each). settled_time: type: string format: date-time description: Timestamp when the market was settled and payouts were processed. fee_cost: $ref: '#/components/schemas/FixedPointDollars' - example: "0.3400" + example: '0.3400' description: Total fees paid in fixed point dollars. value: type: integer nullable: true description: Payout of a single yes contract in cents. - GetPortfolioRestingOrderTotalValueResponse: type: object required: @@ -5286,7 +5575,6 @@ components: total_resting_order_value: type: integer description: Total value of resting orders in cents - GetDepositsResponse: type: object required: @@ -5298,7 +5586,6 @@ components: $ref: '#/components/schemas/Deposit' cursor: type: string - Deposit: type: object required: @@ -5319,7 +5606,9 @@ components: - applied - failed - returned - description: Current status of the deposit. 'applied' means funds are reflected in balance. + description: >- + Current status of the deposit. 'applied' means funds are reflected + in balance. type: type: string enum: @@ -5345,8 +5634,9 @@ components: type: integer format: int64 nullable: true - description: Unix timestamp of when the deposit was finalized (applied, failed, or returned). - + description: >- + Unix timestamp of when the deposit was finalized (applied, failed, + or returned). GetWithdrawalsResponse: type: object required: @@ -5358,7 +5648,6 @@ components: $ref: '#/components/schemas/Withdrawal' cursor: type: string - Withdrawal: type: object required: @@ -5379,7 +5668,9 @@ components: - applied - failed - returned - description: Current status of the withdrawal. 'applied' means funds have been deducted from balance. + description: >- + Current status of the withdrawal. 'applied' means funds have been + deducted from balance. type: type: string enum: @@ -5405,9 +5696,9 @@ components: type: integer format: int64 nullable: true - description: Unix timestamp of when the withdrawal was finalized (applied, failed, or returned). - - # FCM schemas + description: >- + Unix timestamp of when the withdrawal was finalized (applied, + failed, or returned). Order: type: object required: @@ -5442,34 +5733,62 @@ components: type: string side: type: string - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' deprecated: true - description: | - Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order direction](/getting_started/order_direction). This field will not be removed before May 14, 2026. + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. action: type: string - enum: [buy, sell] + enum: + - buy + - sell deprecated: true - description: | - Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order direction](/getting_started/order_direction). This field will not be removed before May 14, 2026. + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. outcome_side: type: string - enum: ['yes', 'no'] - description: | - The outcome side this order is positioned for. buy-yes and sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + enum: + - 'yes' + - 'no' + description: > + The outcome side this order is positioned for. buy-yes and sell-no + produce 'yes'; buy-no and sell-yes produce 'no'. + - `outcome_side` describes directional exposure only; it does not change the order's price. An order at price `p` with `outcome_side=no` is matched by an order at the same price `p` with `outcome_side=yes` — both parties trade at the same price, just on opposite directions. + `outcome_side` describes directional exposure only; it does not + change the order's price. An order at price `p` with + `outcome_side=no` is matched by an order at the same price `p` with + `outcome_side=yes` — both parties trade at the same price, just on + opposite directions. - `outcome_side` and `book_side` will become the canonical way to determine order direction. The legacy `action`, `side`, and `is_yes` fields will be deprecated in a future release — please migrate to these new fields. + + `outcome_side` and `book_side` will become the canonical way to + determine order direction. The legacy `action`, `side`, and `is_yes` + fields will be deprecated in a future release — please migrate to + these new fields. book_side: $ref: '#/components/schemas/BookSide' - description: | - Same directional bit as outcome_side in book vocabulary. 'bid' is equivalent to outcome_side 'yes'; 'ask' is equivalent to outcome_side 'no'. + description: > + Same directional bit as outcome_side in book vocabulary. 'bid' is + equivalent to outcome_side 'yes'; 'ask' is equivalent to + outcome_side 'no'. - `outcome_side` and `book_side` will become the canonical way to determine order direction. The legacy `action`, `side`, and `is_yes` fields will be deprecated in a future release — please migrate to these new fields. + + `outcome_side` and `book_side` will become the canonical way to + determine order direction. The legacy `action`, `side`, and `is_yes` + fields will be deprecated in a future release — please migrate to + these new fields. type: type: string - enum: [limit, market] + enum: + - limit + - market status: $ref: '#/components/schemas/OrderStatus' yes_price_dollars: @@ -5480,13 +5799,17 @@ components: description: The no price for this order in fixed-point dollars fill_count_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts that have been filled + description: >- + String representation of the number of contracts that have been + filled remaining_count_fp: $ref: '#/components/schemas/FixedPointCount' description: String representation of the remaining contracts for this order initial_count_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the initial size of the order (contract units) + description: >- + String representation of the initial size of the order (contract + units) taker_fill_cost_dollars: $ref: '#/components/schemas/FixedPointDollars' description: The cost of filled taker orders in dollars @@ -5524,7 +5847,9 @@ components: description: The order group this order is part of cancel_order_on_pause: type: boolean - description: If this flag is set to true, the order will be canceled if the order is open and trading on the exchange is paused for any reason. + description: >- + If this flag is set to true, the order will be canceled if the order + is open and trading on the exchange is paused for any reason. subaccount_number: type: integer nullable: true @@ -5534,7 +5859,6 @@ components: allOf: - $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true - Milestone: type: object required: @@ -5554,11 +5878,14 @@ components: description: Unique identifier for the milestone. category: type: string - description: 'Category of the milestone. E.g. Sports, Elections, Esports, Crypto.' + description: Category of the milestone. E.g. Sports, Elections, Esports, Crypto. example: Sports type: type: string - description: 'Type of the milestone. E.g. football_game, basketball_game, soccer_tournament_multi_leg, baseball_game, hockey_match, golf_tournament, political_race.' + description: >- + Type of the milestone. E.g. football_game, basketball_game, + soccer_tournament_multi_leg, baseball_game, hockey_match, + golf_tournament, political_race. example: football_game start_date: type: string @@ -5597,12 +5924,13 @@ components: type: array items: type: string - description: List of event tickers directly related to the outcome of this milestone. + description: >- + List of event tickers directly related to the outcome of this + milestone. last_updated_ts: type: string format: date-time description: Last time this structured target was updated. - GetMilestoneResponse: type: object required: @@ -5611,7 +5939,6 @@ components: milestone: $ref: '#/components/schemas/Milestone' description: The milestone data. - GetMilestonesResponse: type: object required: @@ -5625,7 +5952,6 @@ components: cursor: type: string description: Cursor for pagination. - GetOrdersResponse: type: object required: @@ -5638,7 +5964,6 @@ components: $ref: '#/components/schemas/Order' cursor: type: string - GetOrderQueuePositionResponse: type: object required: @@ -5647,7 +5972,6 @@ components: queue_position_fp: $ref: '#/components/schemas/FixedPointCount' description: The number of preceding shares before the order in the queue. - OrderQueuePosition: type: object required: @@ -5664,7 +5988,6 @@ components: queue_position_fp: $ref: '#/components/schemas/FixedPointCount' description: The number of preceding shares before the order in the queue. - GetOrderQueuePositionsResponse: type: object required: @@ -5675,7 +5998,6 @@ components: description: Queue positions for all matching orders items: $ref: '#/components/schemas/OrderQueuePosition' - MarketPosition: type: object required: @@ -5697,7 +6019,9 @@ components: description: Total spent on this market in dollars position_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought in this market. Negative means NO contracts and positive means YES contracts + description: >- + String representation of the number of contracts bought in this + market. Negative means NO contracts and positive means YES contracts market_exposure_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Cost of the aggregate market position in dollars @@ -5707,7 +6031,7 @@ components: resting_orders_count: type: integer format: int32 - description: "[DEPRECATED] Aggregate size of resting orders in contract units" + description: '[DEPRECATED] Aggregate size of resting orders in contract units' deprecated: true fees_paid_dollars: $ref: '#/components/schemas/FixedPointDollars' @@ -5716,7 +6040,6 @@ components: type: string format: date-time description: Last time the position is updated - EventPosition: type: object required: @@ -5735,7 +6058,9 @@ components: description: Total spent on this event in dollars total_cost_shares_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the total number of shares traded on this event (including both YES and NO contracts) + description: >- + String representation of the total number of shares traded on this + event (including both YES and NO contracts) event_exposure_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Cost of the aggregate event position in dollars @@ -5745,7 +6070,6 @@ components: fees_paid_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Fees paid on fill orders, in dollars - GetPositionsResponse: type: object required: @@ -5754,7 +6078,12 @@ components: properties: cursor: type: string - description: The Cursor represents a pointer to the next page of records in the pagination. Use the value returned here in the cursor query parameter for this end-point to get the next page containing limit records. An empty value of this field indicates there is no next page. + description: >- + The Cursor represents a pointer to the next page of records in the + pagination. Use the value returned here in the cursor query + parameter for this end-point to get the next page containing limit + records. An empty value of this field indicates there is no next + page. market_positions: type: array items: @@ -5765,7 +6094,6 @@ components: items: $ref: '#/components/schemas/EventPosition' description: List of event positions - Trade: type: object required: @@ -5788,7 +6116,9 @@ components: description: Unique identifier for the market count_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought or sold in this trade + description: >- + String representation of the number of contracts bought or sold in + this trade yes_price_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Yes price for this trade in dollars @@ -5797,35 +6127,63 @@ components: description: No price for this trade in dollars taker_side: type: string - enum: ['yes', 'no'] - x-enum-varnames: ['TradeTakerSideYes', 'TradeTakerSideNo'] + enum: + - 'yes' + - 'no' + x-enum-varnames: + - TradeTakerSideYes + - TradeTakerSideNo deprecated: true - description: | - Deprecated. Use `taker_outcome_side` (or `taker_book_side`) instead. See [Order direction](/getting_started/order_direction). This field will not be removed before May 14, 2026. + description: > + Deprecated. Use `taker_outcome_side` (or `taker_book_side`) instead. + See [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. taker_outcome_side: type: string - enum: ['yes', 'no'] - x-enum-varnames: ['TradeTakerOutcomeSideYes', 'TradeTakerOutcomeSideNo'] - description: | - The outcome side the taker is positioned for. buy-yes and sell-no produce 'yes'; buy-no and sell-yes produce 'no'. - - `taker_outcome_side` describes directional exposure only; it does not change the trade's price. A trade at price `p` with `taker_outcome_side=no` is matched against the maker at the same price `p` with the opposite direction — both parties trade at the same price. - - `taker_outcome_side` and `taker_book_side` will become the canonical way to determine trade direction. The legacy `taker_side` field will be deprecated in a future release — please migrate to these new fields. + enum: + - 'yes' + - 'no' + x-enum-varnames: + - TradeTakerOutcomeSideYes + - TradeTakerOutcomeSideNo + description: > + The outcome side the taker is positioned for. buy-yes and sell-no + produce 'yes'; buy-no and sell-yes produce 'no'. + + + `taker_outcome_side` describes directional exposure only; it does + not change the trade's price. A trade at price `p` with + `taker_outcome_side=no` is matched against the maker at the same + price `p` with the opposite direction — both parties trade at the + same price. + + + `taker_outcome_side` and `taker_book_side` will become the canonical + way to determine trade direction. The legacy `taker_side` field will + be deprecated in a future release — please migrate to these new + fields. taker_book_side: $ref: '#/components/schemas/BookSide' - description: | - Same directional bit as taker_outcome_side in book vocabulary. 'bid' is equivalent to taker_outcome_side 'yes'; 'ask' is equivalent to taker_outcome_side 'no'. + description: > + Same directional bit as taker_outcome_side in book vocabulary. 'bid' + is equivalent to taker_outcome_side 'yes'; 'ask' is equivalent to + taker_outcome_side 'no'. + - `taker_outcome_side` and `taker_book_side` will become the canonical way to determine trade direction. The legacy `taker_side` field will be deprecated in a future release — please migrate to these new fields. + `taker_outcome_side` and `taker_book_side` will become the canonical + way to determine trade direction. The legacy `taker_side` field will + be deprecated in a future release — please migrate to these new + fields. created_time: type: string format: date-time description: Timestamp when this trade was executed is_block_trade: type: boolean - description: True if this trade was matched off-book as a block trade (e.g. via RFQ / negotiated block proposal); false for trades that filled on the standard order book. - + description: >- + True if this trade was matched off-book as a block trade (e.g. via + RFQ / negotiated block proposal); false for trades that filled on + the standard order book. GetIncentiveProgramsResponse: type: object required: @@ -5838,7 +6196,6 @@ components: next_cursor: type: string description: Cursor for pagination to get the next page of results - IncentiveProgram: type: object required: @@ -5857,13 +6214,19 @@ components: description: Unique identifier for the incentive program market_id: type: string - description: The unique identifier of the market associated with this incentive program + description: >- + The unique identifier of the market associated with this incentive + program market_ticker: type: string - description: The ticker symbol of the market associated with this incentive program + description: >- + The ticker symbol of the market associated with this incentive + program incentive_type: type: string - enum: ['liquidity', 'volume'] + enum: + - liquidity + - volume description: Type of incentive program incentive_description: type: string @@ -5891,8 +6254,9 @@ components: target_size_fp: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: String representation of the target size for the incentive program (optional) - + description: >- + String representation of the target size for the incentive program + (optional) GetTradesResponse: type: object required: @@ -5905,7 +6269,6 @@ components: $ref: '#/components/schemas/Trade' cursor: type: string - Fill: type: object required: @@ -5941,34 +6304,62 @@ components: description: Unique identifier for the market (legacy field name, same as ticker) side: type: string - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' deprecated: true - description: | - Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order direction](/getting_started/order_direction). This field will not be removed before May 14, 2026. + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. action: type: string - enum: ['buy', 'sell'] + enum: + - buy + - sell deprecated: true - description: | - Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order direction](/getting_started/order_direction). This field will not be removed before May 14, 2026. + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. outcome_side: type: string - enum: ['yes', 'no'] - description: | - The outcome side this fill positioned the user for. buy-yes and sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + enum: + - 'yes' + - 'no' + description: > + The outcome side this fill positioned the user for. buy-yes and + sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + - `outcome_side` describes directional exposure only; it does not change the fill's price. A fill at price `p` with `outcome_side=no` is matched against an order at the same price `p` with `outcome_side=yes` — both parties trade at the same price, just on opposite directions. + `outcome_side` describes directional exposure only; it does not + change the fill's price. A fill at price `p` with `outcome_side=no` + is matched against an order at the same price `p` with + `outcome_side=yes` — both parties trade at the same price, just on + opposite directions. - `outcome_side` and `book_side` will become the canonical way to determine fill direction. The legacy `action` and `side` fields will be deprecated in a future release — please migrate to these new fields. + + `outcome_side` and `book_side` will become the canonical way to + determine fill direction. The legacy `action` and `side` fields will + be deprecated in a future release — please migrate to these new + fields. book_side: $ref: '#/components/schemas/BookSide' - description: | - Same directional bit as outcome_side in book vocabulary. 'bid' is equivalent to outcome_side 'yes'; 'ask' is equivalent to outcome_side 'no'. + description: > + Same directional bit as outcome_side in book vocabulary. 'bid' is + equivalent to outcome_side 'yes'; 'ask' is equivalent to + outcome_side 'no'. - `outcome_side` and `book_side` will become the canonical way to determine fill direction. The legacy `action` and `side` fields will be deprecated in a future release — please migrate to these new fields. + + `outcome_side` and `book_side` will become the canonical way to + determine fill direction. The legacy `action` and `side` fields will + be deprecated in a future release — please migrate to these new + fields. count_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought or sold in this fill + description: >- + String representation of the number of contracts bought or sold in + this fill yes_price_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Fill price for the yes side in fixed-point dollars @@ -5977,7 +6368,9 @@ components: description: Fill price for the no side in fixed-point dollars is_taker: type: boolean - description: If true, this fill was a taker (removed liquidity from the order book) + description: >- + If true, this fill was a taker (removed liquidity from the order + book) created_time: type: string format: date-time @@ -5989,12 +6382,13 @@ components: type: integer nullable: true x-omitempty: true - description: Subaccount number (0 for primary, 1-63 for subaccounts). Present for direct users. + description: >- + Subaccount number (0 for primary, 1-63 for subaccounts). Present for + direct users. ts: type: integer format: int64 description: Unix timestamp when this fill was executed (legacy field name) - GetFillsResponse: type: object required: @@ -6007,8 +6401,6 @@ components: $ref: '#/components/schemas/Fill' cursor: type: string - - # Structured Target schemas StructuredTarget: type: object properties: @@ -6023,10 +6415,14 @@ components: description: Type of the structured target. details: type: object - description: Additional details about the structured target. Contains flexible JSON data specific to the target type. + description: >- + Additional details about the structured target. Contains flexible + JSON data specific to the target type. source_id: type: string - description: External source identifier for the structured target, if available (e.g., third-party data provider ID). + description: >- + External source identifier for the structured target, if available + (e.g., third-party data provider ID). source_ids: type: object additionalProperties: @@ -6036,7 +6432,6 @@ components: type: string format: date-time description: Timestamp when this structured target was last updated. - GetStructuredTargetsResponse: type: object properties: @@ -6046,19 +6441,17 @@ components: $ref: '#/components/schemas/StructuredTarget' cursor: type: string - description: Pagination cursor for the next page. Empty if there are no more results. - + description: >- + Pagination cursor for the next page. Empty if there are no more + results. GetStructuredTargetResponse: type: object properties: structured_target: $ref: '#/components/schemas/StructuredTarget' - - # Order Group schemas EmptyResponse: type: object description: An empty response body - IntraExchangeInstanceTransferRequest: type: object required: @@ -6086,7 +6479,6 @@ components: default: 0 x-go-type-skip-optional-pointer: true description: Destination exchange shard index (default 0) - IntraExchangeInstanceTransferResponse: type: object required: @@ -6095,7 +6487,6 @@ components: transfer_id: type: string description: The ID of the transfer that was created - OrderGroup: type: object required: @@ -6108,7 +6499,9 @@ components: 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. + 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 @@ -6118,7 +6511,6 @@ components: allOf: - $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true - GetOrderGroupsResponse: type: object properties: @@ -6127,7 +6519,6 @@ components: items: $ref: '#/components/schemas/OrderGroup' x-go-type-skip-optional-pointer: true - GetOrderGroupResponse: type: object required: @@ -6139,7 +6530,9 @@ components: 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. + 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 @@ -6151,34 +6544,42 @@ components: allOf: - $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true - CreateOrderGroupRequest: type: object properties: subaccount: type: integer minimum: 0 - description: Optional subaccount number to use for this order group (0 for primary, 1-63 for subaccounts) + description: >- + Optional subaccount number to use for this order group (0 for + primary, 1-63 for subaccounts) default: 0 x-go-type-skip-optional-pointer: true contracts_limit: 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: >- + 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. + 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 x-go-type-skip-optional-pointer: true - UpdateOrderGroupLimitRequest: type: object properties: @@ -6186,15 +6587,22 @@ components: 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: >- + 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: 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. - + 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: @@ -6207,13 +6615,14 @@ components: subaccount: type: integer minimum: 0 - description: Subaccount number that owns the created order group (0 for primary, 1-63 for subaccounts). + 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 - GetCommunicationsIDResponse: type: object required: @@ -6222,7 +6631,6 @@ components: communications_id: type: string description: A public communications ID which is used to identify the user - BlockTradeProposal: type: object required: @@ -6249,17 +6657,25 @@ components: 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. + 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. + 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. + 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. + 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 @@ -6275,7 +6691,9 @@ components: maker_side: type: string description: The maker side of the trade - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' expiration_ts: type: string format: date-time @@ -6317,7 +6735,6 @@ components: type: string description: Order ID for the seller after the proposal is executed x-go-type-skip-optional-pointer: true - GetBlockTradeProposalsResponse: type: object required: @@ -6332,7 +6749,6 @@ components: type: string description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - ProposeBlockTradeRequest: type: object required: @@ -6351,13 +6767,18 @@ components: validate: required buyer_subtrader_id: type: string - description: Subtrader ID of the buyer. Provide either this or buyer_subaccount, not both. + 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. + 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 @@ -6365,13 +6786,18 @@ components: validate: required seller_subtrader_id: type: string - description: Subtrader ID of the seller. Provide either this or seller_subaccount, not both. + 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. + 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 @@ -6394,7 +6820,9 @@ components: maker_side: type: string description: The maker side of the trade - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' x-oapi-codegen-extra-tags: validate: required,oneof=yes no expiration_ts: @@ -6403,7 +6831,6 @@ components: description: Expiration time of the proposal x-oapi-codegen-extra-tags: validate: required - ProposeBlockTradeResponse: type: object required: @@ -6412,20 +6839,23 @@ components: block_trade_proposal_id: type: string description: The ID of the newly created block trade proposal - AcceptBlockTradeProposalRequest: type: object properties: subtrader_id: type: string - description: Subtrader ID to accept as. Provide either this or subaccount, not both. + 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: >- + User-managed subaccount number to accept as (0 for primary, 1-63 for + numbered subaccounts). Provide either this or subtrader_id, not + both. RFQ: type: object required: @@ -6447,14 +6877,18 @@ components: 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 + 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 status: type: string description: Current status of the RFQ (open, closed) - enum: [open, closed] + enum: + - open + - closed created_ts: type: string format: date-time @@ -6483,7 +6917,9 @@ components: 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) + description: >- + Subaccount number of the RFQ creator (visible when the caller is the + RFQ creator) cancelled_ts: type: string format: date-time @@ -6492,7 +6928,6 @@ components: type: string format: date-time description: Timestamp when the RFQ was last updated - GetRFQsResponse: type: object required: @@ -6507,7 +6942,6 @@ components: type: string description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - GetRFQResponse: type: object required: @@ -6516,7 +6950,6 @@ components: rfq: $ref: '#/components/schemas/RFQ' description: The details of the requested RFQ - CreateRFQRequest: type: object required: @@ -6528,16 +6961,23 @@ components: 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: >- + 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 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. + 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.' + 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 target_cost_dollars: @@ -6558,9 +6998,10 @@ components: 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 for (direct members only; 0 + for primary, 1-63 for subaccounts) x-go-type-skip-optional-pointer: true - CreateRFQResponse: type: object required: @@ -6569,7 +7010,6 @@ components: id: type: string description: The ID of the newly created RFQ - Quote: type: object required: @@ -6621,11 +7061,18 @@ components: status: type: string description: Current status of the quote - enum: [open, accepted, confirmed, executed, cancelled] + enum: + - open + - accepted + - confirmed + - executed + - cancelled accepted_side: type: string description: The side that was accepted (yes or no) - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' accepted_ts: type: string format: date-time @@ -6647,7 +7094,9 @@ components: description: Whether to rest the remainder of the quote after execution post_only: type: boolean - description: Whether the quote creator's order is post-only (visible when the caller is the quote creator) + 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 @@ -6673,17 +7122,20 @@ components: 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) + 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) + 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) - GetQuotesResponse: type: object required: @@ -6698,7 +7150,6 @@ components: type: string description: Cursor for pagination to get the next page of results x-go-type-skip-optional-pointer: true - GetQuoteResponse: type: object required: @@ -6707,7 +7158,6 @@ components: quote: $ref: '#/components/schemas/Quote' description: The details of the requested quote - CreateQuoteRequest: type: object required: @@ -6732,11 +7182,14 @@ components: 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. + 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 - description: Optional subaccount number to place the quote under (0 for primary, 1-63 for subaccounts) - + description: >- + Optional subaccount number to place the quote under (0 for primary, + 1-63 for subaccounts) CreateQuoteResponse: type: object required: @@ -6745,7 +7198,6 @@ components: id: type: string description: The ID of the newly created quote - AcceptQuoteRequest: type: object required: @@ -6754,9 +7206,9 @@ components: accepted_side: type: string description: The side of the quote to accept (yes or no) - enum: ['yes', 'no'] - - # Order schemas + enum: + - 'yes' + - 'no' GetOrderResponse: type: object required: @@ -6764,7 +7216,6 @@ components: properties: order: $ref: '#/components/schemas/Order' - CreateOrderRequest: type: object required: @@ -6781,25 +7232,33 @@ components: x-go-type-skip-optional-pointer: true side: type: string - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' x-oapi-codegen-extra-tags: validate: required,oneof=yes no action: type: string - enum: ['buy', 'sell'] + enum: + - buy + - sell x-oapi-codegen-extra-tags: validate: required,oneof=buy sell count: type: integer minimum: 1 - description: Order quantity in contracts (whole contracts only). Provide count or count_fp; if both provided they must match. + description: >- + Order quantity in contracts (whole contracts only). Provide count or + count_fp; if both provided they must match. x-go-type-skip-optional-pointer: true x-oapi-codegen-extra-tags: validate: omitempty,gte=1 count_fp: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: String representation of the order quantity in contracts. Provide count or count_fp; if both provided they must match. + description: >- + String representation of the order quantity in contracts. Provide + count or count_fp; if both provided they must match. yes_price: type: integer minimum: 1 @@ -6819,33 +7278,50 @@ components: expiration_ts: type: integer format: int64 - description: | - Optional Unix timestamp in seconds for when the order expires. To place + description: > + Optional Unix timestamp in seconds for when the order expires. To + place + an expiring order, set `time_in_force` to `good_till_canceled` and - provide this `expiration_ts`. `GTT` is an internal execution type and is + + provide this `expiration_ts`. `GTT` is an internal execution type + and is + not a valid API value for `time_in_force`. The `immediate_or_cancel` + time-in-force value cannot be combined with `expiration_ts`. time_in_force: type: string - description: | - Specifies how long the order remains active. Use `good_till_canceled` + description: > + Specifies how long the order remains active. Use + `good_till_canceled` + with `expiration_ts` for an order that should rest until a specific + expiration time; without `expiration_ts`, `good_till_canceled` is a + true good-till-canceled order. `GTT` is not a valid API value. - enum: ['fill_or_kill', 'good_till_canceled', 'immediate_or_cancel'] + enum: + - fill_or_kill + - good_till_canceled + - immediate_or_cancel x-oapi-codegen-extra-tags: - validate: omitempty,oneof=fill_or_kill good_till_canceled immediate_or_cancel + validate: >- + omitempty,oneof=fill_or_kill good_till_canceled + immediate_or_cancel x-go-type-skip-optional-pointer: true buy_max_cost: type: integer - description: Maximum cost in cents. When specified, the order will automatically have Fill-or-Kill (FoK) behavior. + description: >- + Maximum cost in cents. When specified, the order will automatically + have Fill-or-Kill (FoK) behavior. post_only: type: boolean reduce_only: type: boolean sell_position_floor: type: integer - description: "Deprecated: Use reduce_only instead. Only accepts value of 0." + description: 'Deprecated: Use reduce_only instead. Only accepts value of 0.' self_trade_prevention_type: allOf: - $ref: '#/components/schemas/SelfTradePreventionType' @@ -6858,19 +7334,25 @@ components: x-go-type-skip-optional-pointer: true cancel_order_on_pause: type: boolean - description: If this flag is set to true, the order will be canceled if the order is open and trading on the exchange is paused for any reason. + description: >- + If this flag is set to true, the order will be canceled if the order + is open and trading on the exchange is paused for any reason. subaccount: type: integer minimum: 0 default: 0 - description: The subaccount number to use for this order. 0 is the primary subaccount. + description: >- + The subaccount number to use for this order. 0 is the primary + subaccount. x-go-type-skip-optional-pointer: true exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' default: 0 + description: >- + Exchange shard index. Defaults to 0. Use -1 to auto-route by market + ticker. x-go-type-skip-optional-pointer: true - CreateOrderResponse: type: object required: @@ -6878,7 +7360,6 @@ components: properties: order: $ref: '#/components/schemas/Order' - BatchCreateOrdersRequest: type: object required: @@ -6890,7 +7371,6 @@ components: validate: required,dive items: $ref: '#/components/schemas/CreateOrderRequest' - BatchCreateOrdersResponse: type: object required: @@ -6900,7 +7380,6 @@ components: type: array items: $ref: '#/components/schemas/BatchCreateOrdersIndividualResponse' - BatchCreateOrdersIndividualResponse: type: object properties: @@ -6909,13 +7388,12 @@ components: nullable: true order: allOf: - - $ref: '#/components/schemas/Order' + - $ref: '#/components/schemas/Order' nullable: true error: allOf: - - $ref: '#/components/schemas/ErrorResponse' + - $ref: '#/components/schemas/ErrorResponse' nullable: true - BatchCancelOrdersRequest: type: object properties: @@ -6929,8 +7407,9 @@ components: type: array items: $ref: '#/components/schemas/BatchCancelOrdersRequestOrder' - description: An array of orders to cancel, each optionally specifying a subaccount - + description: >- + An array of orders to cancel, each optionally specifying a + subaccount BatchCancelOrdersRequestOrder: type: object required: @@ -6943,14 +7422,22 @@ components: type: integer minimum: 0 default: 0 - description: Optional subaccount number to use for this cancellation (0 for primary, 1-63 for subaccounts) + description: >- + Optional subaccount number to use for this cancellation (0 for + primary, 1-63 for subaccounts) x-go-type-skip-optional-pointer: true exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' default: 0 + description: >- + Exchange shard index. Defaults to 0. Use -1 to auto-route by market + ticker. + x-go-type-skip-optional-pointer: true + market_ticker: + type: string + description: Market ticker. Required when exchange_index is -1 (auto). x-go-type-skip-optional-pointer: true - BatchCancelOrdersResponse: type: object required: @@ -6960,7 +7447,6 @@ components: type: array items: $ref: '#/components/schemas/BatchCancelOrdersIndividualResponse' - BatchCancelOrdersIndividualResponse: type: object required: @@ -6969,31 +7455,22 @@ components: properties: order_id: type: string - description: The order ID to identify which order had an error during batch cancellation + description: >- + The order ID to identify which order had an error during batch + cancellation order: allOf: - $ref: '#/components/schemas/Order' nullable: true reduced_by_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts that were successfully canceled from this order + description: >- + String representation of the number of contracts that were + successfully canceled from this order error: allOf: - $ref: '#/components/schemas/ErrorResponse' nullable: true - - CancelOrderResponse: - type: object - required: - - order - - reduced_by_fp - properties: - order: - $ref: '#/components/schemas/Order' - reduced_by_fp: - $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts that were successfully canceled from this order - AmendOrderRequest: type: object required: @@ -7004,7 +7481,9 @@ components: subaccount: type: integer minimum: 0 - description: Optional subaccount number to use for this amendment (0 for primary, 1-63 for subaccounts) + description: >- + Optional subaccount number to use for this amendment (0 for primary, + 1-63 for subaccounts) default: 0 x-go-type-skip-optional-pointer: true ticker: @@ -7012,11 +7491,15 @@ components: description: Market ticker side: type: string - enum: ["yes", "no"] + enum: + - 'yes' + - 'no' description: Side of the order action: type: string - enum: ["buy", "sell"] + enum: + - buy + - sell description: Action of the order client_order_id: type: string @@ -7038,24 +7521,35 @@ components: description: Updated no price for the order in cents yes_price_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Updated yes price for the order in fixed-point dollars. Exactly one of yes_price, no_price, yes_price_dollars, and no_price_dollars must be passed. + description: >- + Updated yes price for the order in fixed-point dollars. Exactly one + of yes_price, no_price, yes_price_dollars, and no_price_dollars must + be passed. no_price_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Updated no price for the order in fixed-point dollars. Exactly one of yes_price, no_price, yes_price_dollars, and no_price_dollars must be passed. + description: >- + Updated no price for the order in fixed-point dollars. Exactly one + of yes_price, no_price, yes_price_dollars, and no_price_dollars must + be passed. count: type: integer minimum: 1 - description: Updated quantity for the order (whole contracts only). If updating quantity, provide count or count_fp; if both provided they must match. + description: >- + Updated quantity for the order (whole contracts only). If updating + quantity, provide count or count_fp; if both provided they must + match. count_fp: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: String representation of the updated quantity for the order. If updating quantity, provide count or count_fp; if both provided they must match. + description: >- + String representation of the updated quantity for the order. If + updating quantity, provide count or count_fp; if both provided they + must match. exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' default: 0 x-go-type-skip-optional-pointer: true - AmendOrderResponse: type: object required: @@ -7068,46 +7562,6 @@ components: order: $ref: '#/components/schemas/Order' description: The order after amendment - - DecreaseOrderRequest: - type: object - properties: - subaccount: - type: integer - minimum: 0 - description: Optional subaccount number to use for this decrease (0 for primary, 1-63 for subaccounts) - default: 0 - x-go-type-skip-optional-pointer: true - reduce_by: - type: integer - minimum: 1 - description: Number of contracts to reduce by (whole contracts only). Reduce-by may be provided via reduce_by or reduce_by_fp; if both provided they must match. Exactly one of reduce_by(/reduce_by_fp) or reduce_to(/reduce_to_fp) must be provided. - reduce_by_fp: - $ref: '#/components/schemas/FixedPointCount' - nullable: true - description: String representation of the number of contracts to reduce by. Reduce-by may be provided via reduce_by or reduce_by_fp; if both provided they must match. Exactly one of reduce_by(/reduce_by_fp) or reduce_to(/reduce_to_fp) must be provided. - reduce_to: - type: integer - minimum: 0 - description: Number of contracts to reduce to (whole contracts only). Reduce-to may be provided via reduce_to or reduce_to_fp; if both provided they must match. Exactly one of reduce_by(/reduce_by_fp) or reduce_to(/reduce_to_fp) must be provided. - reduce_to_fp: - $ref: '#/components/schemas/FixedPointCount' - nullable: true - description: String representation of the number of contracts to reduce to. Reduce-to may be provided via reduce_to or reduce_to_fp; if both provided they must match. Exactly one of reduce_by(/reduce_by_fp) or reduce_to(/reduce_to_fp) must be provided. - exchange_index: - allOf: - - $ref: '#/components/schemas/ExchangeIndex' - default: 0 - x-go-type-skip-optional-pointer: true - - DecreaseOrderResponse: - type: object - required: - - order - properties: - order: - $ref: '#/components/schemas/Order' - CreateOrderV2Request: type: object required: @@ -7121,8 +7575,8 @@ components: ticker: HIGHNY-24JAN01-T60 client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 side: bid - count: "10.00" - price: "0.5600" + count: '10.00' + price: '0.5600' time_in_force: good_till_canceled self_trade_prevention_type: taker_at_cross post_only: false @@ -7152,21 +7606,37 @@ components: expiration_time: type: integer format: int64 - description: | - Optional Unix timestamp in seconds for when the order expires. To place + description: > + Optional Unix timestamp in seconds for when the order expires. To + place + an expiring order, set `time_in_force` to `good_till_canceled` and - provide this `expiration_time`. `GTT` is an internal execution type and + + provide this `expiration_time`. `GTT` is an internal execution type + and + is not a valid API value for `time_in_force`. The + `immediate_or_cancel` time-in-force value cannot be combined with + `expiration_time`. time_in_force: type: string - description: | - Specifies how long the order remains active. Use `good_till_canceled` - with `expiration_time` for an order that should rest until a specific - expiration time; without `expiration_time`, `good_till_canceled` is a + description: > + Specifies how long the order remains active. Use + `good_till_canceled` + + with `expiration_time` for an order that should rest until a + specific + + expiration time; without `expiration_time`, `good_till_canceled` is + a + true good-till-canceled order. `GTT` is not a valid API value. - enum: ['fill_or_kill', 'good_till_canceled', 'immediate_or_cancel'] + enum: + - fill_or_kill + - good_till_canceled + - immediate_or_cancel x-oapi-codegen-extra-tags: validate: required,oneof=fill_or_kill good_till_canceled immediate_or_cancel x-go-type-skip-optional-pointer: true @@ -7180,15 +7650,21 @@ components: x-go-type-skip-optional-pointer: true cancel_order_on_pause: type: boolean - description: If this flag is set to true, the order will be canceled if the order is open and trading on the exchange is paused for any reason. + description: >- + If this flag is set to true, the order will be canceled if the order + is open and trading on the exchange is paused for any reason. reduce_only: type: boolean - description: Specifies whether the order place count should be capped by the member's current position. + description: >- + Specifies whether the order place count should be capped by the + member's current position. subaccount: type: integer minimum: 0 default: 0 - description: The subaccount number to use for this order. 0 is the primary subaccount. + description: >- + The subaccount number to use for this order. 0 is the primary + subaccount. x-go-type-skip-optional-pointer: true order_group_id: type: string @@ -7198,8 +7674,10 @@ components: allOf: - $ref: '#/components/schemas/ExchangeIndex' default: 0 + description: >- + Exchange shard index. Defaults to 0. Use -1 to auto-route by market + ticker. x-go-type-skip-optional-pointer: true - CreateOrderV2Response: type: object required: @@ -7210,8 +7688,8 @@ components: example: order_id: 3b23c1c7-f4ef-4f0d-8b9a-9e53c61f1a0d client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 - fill_count: "0.00" - remaining_count: "10.00" + fill_count: '0.00' + remaining_count: '10.00' ts_ms: 1715793600123 properties: order_id: @@ -7223,18 +7701,25 @@ components: description: Number of contracts filled immediately upon placement. remaining_count: $ref: '#/components/schemas/FixedPointCount' - description: Number of contracts remaining after placement. For IOC orders, this reflects the final state after unfilled contracts are canceled. + description: >- + Number of contracts remaining after placement. For IOC orders, this + reflects the final state after unfilled contracts are canceled. average_fill_price: $ref: '#/components/schemas/FixedPointDollars' - description: Volume-weighted average fill price. Only present when fill_count > 0. + description: >- + Volume-weighted average fill price. Only present when fill_count > + 0. average_fee_paid: $ref: '#/components/schemas/FixedPointDollars' - description: Volume-weighted average fee paid per contract for fills resulting from this request. Only present when fill_count > 0. + description: >- + Volume-weighted average fee paid per contract for fills resulting + from this request. Only present when fill_count > 0. ts_ms: type: integer format: int64 - description: Matching engine timestamp at which the order was processed, as Unix epoch milliseconds. - + description: >- + Matching engine timestamp at which the order was processed, as Unix + epoch milliseconds. CancelOrderV2Response: type: object required: @@ -7244,7 +7729,7 @@ components: example: order_id: 3b23c1c7-f4ef-4f0d-8b9a-9e53c61f1a0d client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 - reduced_by: "10.00" + reduced_by: '10.00' ts_ms: 1715793660456 properties: order_id: @@ -7253,32 +7738,38 @@ components: type: string reduced_by: $ref: '#/components/schemas/FixedPointCount' - description: Number of contracts that were canceled (i.e. the remaining count at time of cancellation). + description: >- + Number of contracts that were canceled (i.e. the remaining count at + time of cancellation). ts_ms: type: integer format: int64 - description: Matching engine timestamp at which the cancellation was processed, as Unix epoch milliseconds. - + description: >- + Matching engine timestamp at which the cancellation was processed, + as Unix epoch milliseconds. DecreaseOrderV2Request: type: object example: - reduce_by: "2.00" + reduce_by: '2.00' exchange_index: 0 properties: reduce_by: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: String representation of the number of contracts to reduce by. Exactly one of `reduce_by` or `reduce_to` must be provided. + description: >- + String representation of the number of contracts to reduce by. + Exactly one of `reduce_by` or `reduce_to` must be provided. reduce_to: $ref: '#/components/schemas/FixedPointCount' nullable: true - description: String representation of the number of contracts to reduce to. Exactly one of `reduce_by` or `reduce_to` must be provided. + description: >- + String representation of the number of contracts to reduce to. + Exactly one of `reduce_by` or `reduce_to` must be provided. exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' default: 0 x-go-type-skip-optional-pointer: true - DecreaseOrderV2Response: type: object required: @@ -7288,7 +7779,7 @@ components: example: order_id: 3b23c1c7-f4ef-4f0d-8b9a-9e53c61f1a0d client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 - remaining_count: "8.00" + remaining_count: '8.00' ts_ms: 1715793680789 properties: order_id: @@ -7301,8 +7792,9 @@ components: ts_ms: type: integer format: int64 - description: Matching engine timestamp at which the decrease was processed, as Unix epoch milliseconds. - + description: >- + Matching engine timestamp at which the decrease was processed, as + Unix epoch milliseconds. AmendOrderV2Request: type: object required: @@ -7313,8 +7805,8 @@ components: example: ticker: HIGHNY-24JAN01-T60 side: bid - price: "0.5700" - count: "8.00" + price: '0.5700' + count: '8.00' client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 updated_client_order_id: 2a0e3fc9-b593-4aa3-96e5-82f7f7566c2a exchange_index: 0 @@ -7335,7 +7827,10 @@ components: x-go-type-skip-optional-pointer: true count: $ref: '#/components/schemas/FixedPointCount' - description: Updated total/max fillable count for the order. Set this to the order's already filled count plus the desired resting remaining count after the amend. + description: >- + Updated total/max fillable count for the order. Set this to the + order's already filled count plus the desired resting remaining + count after the amend. x-go-type-skip-optional-pointer: true client_order_id: type: string @@ -7350,7 +7845,6 @@ components: - $ref: '#/components/schemas/ExchangeIndex' default: 0 x-go-type-skip-optional-pointer: true - AmendOrderV2Response: type: object required: @@ -7359,8 +7853,8 @@ components: example: order_id: 3b23c1c7-f4ef-4f0d-8b9a-9e53c61f1a0d client_order_id: 2a0e3fc9-b593-4aa3-96e5-82f7f7566c2a - remaining_count: "8.00" - fill_count: "0.00" + remaining_count: '8.00' + fill_count: '0.00' ts_ms: 1715793690123 properties: order_id: @@ -7371,27 +7865,38 @@ components: $ref: '#/components/schemas/FixedPointCount' nullable: true x-omitempty: false - description: Number of resting contracts remaining after the amend. This is the actual post-amend resting quantity, not the request's total/max fillable count. Only present when the amend caused a fill or changed the resting size. + description: >- + Number of resting contracts remaining after the amend. This is the + actual post-amend resting quantity, not the request's total/max + fillable count. Only present when the amend caused a fill or changed + the resting size. fill_count: $ref: '#/components/schemas/FixedPointCount' nullable: true x-omitempty: false - description: Number of contracts filled as a result of the amend crossing the book. Only present when fills occurred or remaining size changed. + description: >- + Number of contracts filled as a result of the amend crossing the + book. Only present when fills occurred or remaining size changed. average_fill_price: $ref: '#/components/schemas/FixedPointDollars' nullable: true x-omitempty: false - description: Volume-weighted average fill price for fills resulting from the amend. Only present when fills occurred. + description: >- + Volume-weighted average fill price for fills resulting from the + amend. Only present when fills occurred. average_fee_paid: $ref: '#/components/schemas/FixedPointDollars' nullable: true x-omitempty: false - description: Volume-weighted average fee paid per contract for fills resulting from the amend. Only present when fills occurred. + description: >- + Volume-weighted average fee paid per contract for fills resulting + from the amend. Only present when fills occurred. ts_ms: type: integer format: int64 - description: Matching engine timestamp at which the amend was processed, as Unix epoch milliseconds. - + description: >- + Matching engine timestamp at which the amend was processed, as Unix + epoch milliseconds. BatchCreateOrdersV2Request: type: object required: @@ -7401,16 +7906,16 @@ components: - ticker: HIGHNY-24JAN01-T60 client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 side: bid - count: "10.00" - price: "0.5600" + count: '10.00' + price: '0.5600' time_in_force: good_till_canceled self_trade_prevention_type: taker_at_cross exchange_index: 0 - ticker: HIGHNY-24JAN01-T60 client_order_id: 2a0e3fc9-b593-4aa3-96e5-82f7f7566c2a side: ask - count: "5.00" - price: "0.5800" + count: '5.00' + price: '0.5800' time_in_force: immediate_or_cancel self_trade_prevention_type: maker exchange_index: 0 @@ -7421,7 +7926,6 @@ components: validate: required,dive items: $ref: '#/components/schemas/CreateOrderV2Request' - BatchCreateOrdersV2Response: type: object required: @@ -7430,15 +7934,15 @@ components: orders: - order_id: 3b23c1c7-f4ef-4f0d-8b9a-9e53c61f1a0d client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 - fill_count: "0.00" - remaining_count: "10.00" + fill_count: '0.00' + remaining_count: '10.00' ts_ms: 1715793600123 - order_id: a6d6010d-6d5f-40a1-a7e7-5501386bb621 client_order_id: 2a0e3fc9-b593-4aa3-96e5-82f7f7566c2a - fill_count: "5.00" - remaining_count: "0.00" - average_fill_price: "0.5800" - average_fee_paid: "0.0012" + fill_count: '5.00' + remaining_count: '0.00' + average_fill_price: '0.5800' + average_fee_paid: '0.0012' ts_ms: 1715793600456 properties: orders: @@ -7465,23 +7969,28 @@ components: $ref: '#/components/schemas/FixedPointDollars' nullable: true x-omitempty: false - description: Volume-weighted average fill price. Only present when fill_count > 0. + description: >- + Volume-weighted average fill price. Only present when + fill_count > 0. average_fee_paid: $ref: '#/components/schemas/FixedPointDollars' nullable: true x-omitempty: false - description: Volume-weighted average fee paid per contract. Only present when fill_count > 0. + description: >- + Volume-weighted average fee paid per contract. Only present + when fill_count > 0. ts_ms: type: integer format: int64 nullable: true x-omitempty: false - description: Matching engine timestamp at which the order was processed, as Unix epoch milliseconds. Absent when the request errored. + description: >- + Matching engine timestamp at which the order was processed, as + Unix epoch milliseconds. Absent when the request errored. error: allOf: - $ref: '#/components/schemas/ErrorResponse' nullable: true - BatchCancelOrdersV2Request: type: object required: @@ -7499,7 +8008,9 @@ components: type: array x-oapi-codegen-extra-tags: validate: required,dive - description: An array of orders to cancel, each optionally specifying a subaccount. + description: >- + An array of orders to cancel, each optionally specifying a + subaccount. items: type: object required: @@ -7512,14 +8023,22 @@ components: type: integer minimum: 0 default: 0 - description: Optional subaccount number to use for this cancellation (0 for primary, 1-63 for subaccounts). + description: >- + Optional subaccount number to use for this cancellation (0 for + primary, 1-63 for subaccounts). x-go-type-skip-optional-pointer: true exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' default: 0 + description: >- + Exchange shard index. Defaults to 0. Use -1 to auto-route by + market ticker. + x-go-type-skip-optional-pointer: true + market_ticker: + type: string + description: Market ticker. Required when exchange_index is -1 (auto). x-go-type-skip-optional-pointer: true - BatchCancelOrdersV2Response: type: object required: @@ -7528,11 +8047,11 @@ components: orders: - order_id: 3b23c1c7-f4ef-4f0d-8b9a-9e53c61f1a0d client_order_id: 8c35ecb3-328f-4f52-8c7c-0f4b9862f8d1 - reduced_by: "10.00" + reduced_by: '10.00' ts_ms: 1715793660456 - order_id: a6d6010d-6d5f-40a1-a7e7-5501386bb621 client_order_id: 2a0e3fc9-b593-4aa3-96e5-82f7f7566c2a - reduced_by: "5.00" + reduced_by: '5.00' ts_ms: 1715793660789 properties: orders: @@ -7545,25 +8064,30 @@ components: properties: order_id: type: string - description: The order ID identifying which order this entry corresponds to. + description: >- + The order ID identifying which order this entry corresponds + to. client_order_id: type: string nullable: true reduced_by: $ref: '#/components/schemas/FixedPointCount' - description: Number of contracts that were canceled (i.e. the remaining count at time of cancellation). Zero if the cancel errored. + description: >- + Number of contracts that were canceled (i.e. the remaining + count at time of cancellation). Zero if the cancel errored. ts_ms: type: integer format: int64 nullable: true x-omitempty: false - description: Matching engine timestamp at which the cancellation was processed, as Unix epoch milliseconds. Absent when the cancel errored. + description: >- + Matching engine timestamp at which the cancellation was + processed, as Unix epoch milliseconds. Absent when the cancel + errored. error: allOf: - $ref: '#/components/schemas/ErrorResponse' nullable: true - - # Multivariate Event Collection schemas AssociatedEvent: type: object required: @@ -7581,18 +8105,21 @@ components: type: integer format: int32 nullable: true - description: Maximum number of markets from this event (inclusive). Null means no limit. + description: >- + Maximum number of markets from this event (inclusive). Null means no + limit. size_min: type: integer format: int32 nullable: true - description: Minimum number of markets from this event (inclusive). Null means no limit. + description: >- + Minimum number of markets from this event (inclusive). Null means no + limit. active_quoters: type: array items: type: string description: List of active quoters for this event. - MultivariateEventCollection: type: object required: @@ -7616,7 +8143,9 @@ components: description: Unique identifier for the collection. series_ticker: type: string - description: Series associated with the collection. Events produced in the collection will be associated with this series. + description: >- + Series associated with the collection. Events produced in the + collection will be associated with this series. title: type: string description: Title of the collection. @@ -7626,11 +8155,15 @@ components: open_date: type: string format: date-time - description: The open date of the collection. Before this time, the collection cannot be interacted with. + description: >- + The open date of the collection. Before this time, the collection + cannot be interacted with. close_date: type: string format: date-time - description: The close date of the collection. After this time, the collection cannot be interacted with. + description: >- + The close date of the collection. After this time, the collection + cannot be interacted with. associated_events: type: array items: @@ -7640,28 +8173,44 @@ components: type: array items: type: string - description: '[DEPRECATED - Use associated_events instead] A list of events associated with the collection. Markets in these events can be passed as inputs to the Lookup and Create endpoints.' + description: >- + [DEPRECATED - Use associated_events instead] A list of events + associated with the collection. Markets in these events can be + passed as inputs to the Lookup and Create endpoints. is_ordered: type: boolean - description: Whether the collection is ordered. If true, the order of markets passed into Lookup/Create affects the output. If false, the order does not matter. + description: >- + Whether the collection is ordered. If true, the order of markets + passed into Lookup/Create affects the output. If false, the order + does not matter. is_single_market_per_event: type: boolean - description: '[DEPRECATED - Use associated_events instead] Whether the collection accepts multiple markets from the same event passed into Lookup/Create.' + description: >- + [DEPRECATED - Use associated_events instead] Whether the collection + accepts multiple markets from the same event passed into + Lookup/Create. is_all_yes: type: boolean - description: '[DEPRECATED - Use associated_events instead] Whether the collection requires that only the market side of ''yes'' may be used.' + description: >- + [DEPRECATED - Use associated_events instead] Whether the collection + requires that only the market side of 'yes' may be used. size_min: type: integer format: int32 - description: The minimum number of markets that must be passed into Lookup/Create (inclusive). + description: >- + The minimum number of markets that must be passed into Lookup/Create + (inclusive). size_max: type: integer format: int32 - description: The maximum number of markets that must be passed into Lookup/Create (inclusive). + description: >- + The maximum number of markets that must be passed into Lookup/Create + (inclusive). functional_description: type: string - description: A functional description of the collection describing how inputs affect the output. - + description: >- + A functional description of the collection describing how inputs + affect the output. GetMultivariateEventCollectionResponse: type: object required: @@ -7670,7 +8219,6 @@ components: multivariate_contract: $ref: '#/components/schemas/MultivariateEventCollection' description: The multivariate event collection. - GetMultivariateEventCollectionsResponse: type: object required: @@ -7683,9 +8231,13 @@ components: description: List of multivariate event collections. cursor: type: string - description: The Cursor represents a pointer to the next page of records in the pagination. Use the value returned here in the cursor query parameter for this end-point to get the next page containing limit records. An empty value of this field indicates there is no next page. + description: >- + The Cursor represents a pointer to the next page of records in the + pagination. Use the value returned here in the cursor query + parameter for this end-point to get the next page containing limit + records. An empty value of this field indicates there is no next + page. x-go-type-skip-optional-pointer: true - TickerPair: type: object required: @@ -7701,11 +8253,12 @@ components: description: Event ticker identifier. side: type: string - enum: ['yes', 'no'] + enum: + - 'yes' + - 'no' description: Side of the market (yes or no). x-oapi-codegen-extra-tags: validate: required,oneof=yes no - LookupTickersForMarketInMultivariateEventCollectionRequest: type: object required: @@ -7715,8 +8268,9 @@ components: type: array items: $ref: '#/components/schemas/TickerPair' - description: List of selected markets that act as parameters to determine which market is produced. - + description: >- + List of selected markets that act as parameters to determine which + market is produced. LookupTickersForMarketInMultivariateEventCollectionResponse: type: object required: @@ -7729,7 +8283,6 @@ components: market_ticker: type: string description: Market ticker for the looked up market. - LookupPoint: type: object required: @@ -7753,7 +8306,6 @@ components: type: string format: date-time description: Timestamp when this lookup was last queried. - GetMultivariateEventCollectionLookupHistoryResponse: type: object required: @@ -7764,7 +8316,6 @@ components: items: $ref: '#/components/schemas/LookupPoint' description: List of recent lookup points in the collection. - CreateMarketInMultivariateEventCollectionRequest: type: object required: @@ -7774,13 +8325,14 @@ components: type: array items: $ref: '#/components/schemas/TickerPair' - description: List of selected markets that act as parameters to determine which market is created. + description: >- + List of selected markets that act as parameters to determine which + market is created. x-oapi-codegen-extra-tags: validate: required,dive with_market_payload: type: boolean description: Whether to include the market payload in the response. - CreateMarketInMultivariateEventCollectionResponse: type: object required: @@ -7796,16 +8348,20 @@ components: market: $ref: '#/components/schemas/Market' description: Market payload of the created market. - # Market Orderbook schemas PriceLevelDollarsCountFp: type: array minItems: 2 maxItems: 2 - example: ["0.1500", "100.00"] + example: + - '0.1500' + - '100.00' items: type: string - description: Price level in dollars represented as [dollars_string, fp] where dollars_string is like "0.1500" and fp is a FixedPointCount string (fixed-point contract count). The second element is the contract quantity (not price). - + description: >- + Price level in dollars represented as [dollars_string, fp] where + dollars_string is like "0.1500" and fp is a FixedPointCount string + (fixed-point contract count). The second element is the contract + quantity (not price). OrderbookCountFp: type: object required: @@ -7820,8 +8376,9 @@ components: type: array items: $ref: '#/components/schemas/PriceLevelDollarsCountFp' - description: Orderbook with fixed-point contract counts (fp) in all dollar price levels. - + description: >- + Orderbook with fixed-point contract counts (fp) in all dollar price + levels. GetMarketOrderbooksResponse: type: object required: @@ -7831,7 +8388,6 @@ components: type: array items: $ref: '#/components/schemas/MarketOrderbookFp' - MarketOrderbookFp: type: object required: @@ -7842,7 +8398,6 @@ components: type: string orderbook_fp: $ref: '#/components/schemas/OrderbookCountFp' - GetMarketOrderbookResponse: type: object required: @@ -7851,7 +8406,6 @@ components: orderbook_fp: $ref: '#/components/schemas/OrderbookCountFp' description: Orderbook with fixed-point contract counts (fp) in all price levels. - GetEventsResponse: type: object required: @@ -7870,8 +8424,9 @@ components: $ref: '#/components/schemas/Milestone' cursor: type: string - description: Pagination cursor for the next page. Empty if there are no more results. - + description: >- + Pagination cursor for the next page. Empty if there are no more + results. GetMultivariateEventsResponse: type: object required: @@ -7885,8 +8440,9 @@ components: $ref: '#/components/schemas/EventData' cursor: type: string - description: Pagination cursor for the next page. Empty if there are no more results. - + description: >- + Pagination cursor for the next page. Empty if there are no more + results. EventFeeChange: type: object required: @@ -7911,17 +8467,21 @@ components: - $ref: '#/components/schemas/FeeType' nullable: true example: quadratic - description: New fee type override for the event. When null, the event clears any prior override and falls back to the parent series' fee structure. + description: >- + New fee type override for the event. When null, the event clears any + prior override and falls back to the parent series' fee structure. fee_multiplier_override: type: number format: double nullable: true - description: New fee multiplier override for the event. When null, the event clears any prior override and falls back to the parent series' fee multiplier. + description: >- + New fee multiplier override for the event. When null, the event + clears any prior override and falls back to the parent series' fee + multiplier. scheduled_ts: type: string format: date-time description: Timestamp when this fee change is scheduled to take effect - GetEventFeeChangesResponse: type: object required: @@ -7934,8 +8494,9 @@ components: $ref: '#/components/schemas/EventFeeChange' cursor: type: string - description: Pagination cursor for the next page. Empty if there are no more results. - + description: >- + Pagination cursor for the next page. Empty if there are no more + results. GetEventResponse: type: object required: @@ -7947,10 +8508,13 @@ components: description: Data for the event. markets: type: array - description: Data for the markets in this event. This field is deprecated in favour of the "markets" field inside the event. Which will be filled with the same value if you use the query parameter "with_nested_markets=true". + description: >- + Data for the markets in this event. This field is deprecated in + favour of the "markets" field inside the event. Which will be filled + with the same value if you use the query parameter + "with_nested_markets=true". items: $ref: '#/components/schemas/Market' - MarketMetadata: type: object required: @@ -7967,7 +8531,6 @@ components: color_code: type: string description: The color code for the market. - GetEventMetadataResponse: type: object required: @@ -8003,7 +8566,6 @@ components: x-omitempty: true description: Event scope, based on the competition. x-go-type-skip-optional-pointer: true - GetEventForecastPercentilesHistoryResponse: type: object required: @@ -8014,7 +8576,6 @@ components: description: Array of forecast percentile data points over time. items: $ref: '#/components/schemas/ForecastPercentilesPoint' - ForecastPercentilesPoint: type: object required: @@ -8039,7 +8600,6 @@ components: description: Array of forecast values at different percentiles. items: $ref: '#/components/schemas/PercentilePoint' - PercentilePoint: type: object required: @@ -8061,7 +8621,6 @@ components: formatted_forecast: type: string description: The human-readable formatted forecast value. - EventData: type: object required: @@ -8088,10 +8647,14 @@ components: description: Full title of the event. collateral_return_type: type: string - description: Specifies how collateral is returned when markets settle (e.g., 'binary' for standard yes/no markets). + description: >- + Specifies how collateral is returned when markets settle (e.g., + 'binary' for standard yes/no markets). mutually_exclusive: type: boolean - description: If true, only one market in this event can resolve to 'yes'. If false, multiple markets can resolve to 'yes'. + description: >- + If true, only one market in this event can resolve to 'yes'. If + false, multiple markets can resolve to 'yes'. category: type: string description: Event category (deprecated, use series-level category instead). @@ -8102,16 +8665,23 @@ components: format: date-time nullable: true x-omitempty: true - description: The specific date this event is based on. Only filled when the event uses a date strike (mutually exclusive with strike_period). + description: >- + The specific date this event is based on. Only filled when the event + uses a date strike (mutually exclusive with strike_period). strike_period: type: string nullable: true x-omitempty: true - description: The time period this event covers (e.g., 'week', 'month'). Only filled when the event uses a period strike (mutually exclusive with strike_date). + description: >- + The time period this event covers (e.g., 'week', 'month'). Only + filled when the event uses a period strike (mutually exclusive with + strike_date). markets: type: array x-omitempty: true - description: Array of markets associated with this event. Only populated when 'with_nested_markets=true' is specified in the request. + description: >- + Array of markets associated with this event. Only populated when + 'with_nested_markets=true' is specified in the request. items: $ref: '#/components/schemas/Market' x-go-type-skip-optional-pointer: true @@ -8129,7 +8699,9 @@ components: nullable: true items: $ref: '#/components/schemas/SettlementSource' - description: The official sources used for the determination of markets within this event. Methodology is defined in the rulebook. + description: >- + The official sources used for the determination of markets within + this event. Methodology is defined in the rulebook. last_updated_ts: type: string format: date-time @@ -8138,18 +8710,21 @@ components: type: string nullable: true x-omitempty: true - description: Fee type override for this event. When present, takes precedence over the series-level fee for this event's markets. + description: >- + Fee type override for this event. When present, takes precedence + over the series-level fee for this event's markets. fee_multiplier_override: type: number format: double nullable: true x-omitempty: true - description: Fee multiplier override for this event. Paired with fee_type_override. + description: >- + Fee multiplier override for this event. Paired with + fee_type_override. exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true - Series: type: object required: @@ -8170,10 +8745,16 @@ components: description: Ticker that identifies this series. frequency: type: string - description: Description of the frequency of the series. There is no fixed value set here, but will be something human-readable like weekly, daily, one-off. + description: >- + Description of the frequency of the series. There is no fixed value + set here, but will be something human-readable like weekly, daily, + one-off. title: type: string - description: Title describing the series. For full context use you should use this field with the title field of the events belonging to this series. + description: >- + Title describing the series. For full context use you should use + this field with the title field of the events belonging to this + series. category: type: string description: Category specifies the category which this series belongs to. @@ -8182,19 +8763,28 @@ components: nullable: true items: type: string - description: Tags specifies the subjects that this series relates to, multiple series from different categories can have the same tags. + description: >- + Tags specifies the subjects that this series relates to, multiple + series from different categories can have the same tags. settlement_sources: type: array nullable: true items: $ref: '#/components/schemas/SettlementSource' - description: SettlementSources specifies the official sources used for the determination of markets within the series. Methodology is defined in the rulebook. + description: >- + SettlementSources specifies the official sources used for the + determination of markets within the series. Methodology is defined + in the rulebook. contract_url: type: string - description: ContractUrl provides a direct link to the original filing of the contract which underlies the series. + description: >- + ContractUrl provides a direct link to the original filing of the + contract which underlies the series. contract_terms_url: type: string - description: ContractTermsUrl is the URL to the current terms of the contract underlying the series. + description: >- + ContractTermsUrl is the URL to the current terms of the contract + underlying the series. product_metadata: type: object nullable: true @@ -8203,24 +8793,36 @@ components: fee_type: allOf: - $ref: '#/components/schemas/FeeType' - description: "FeeType is a string representing the series' fee structure. Fee structures can be found at https://kalshi.com/docs/kalshi-fee-schedule.pdf. 'quadratic' is described by the General Trading Fees Table, 'quadratic_with_maker_fees' is described by the General Trading Fees Table with maker fees described in the Maker Fees section, 'flat' is described by the Specific Trading Fees Table." + description: >- + FeeType is a string representing the series' fee structure. Fee + structures can be found at + https://kalshi.com/docs/kalshi-fee-schedule.pdf. 'quadratic' is + described by the General Trading Fees Table, + 'quadratic_with_maker_fees' is described by the General Trading Fees + Table with maker fees described in the Maker Fees section, 'flat' is + described by the Specific Trading Fees Table. fee_multiplier: type: number format: double - description: FeeMultiplier is a floating point multiplier applied to the fee calculations. + description: >- + FeeMultiplier is a floating point multiplier applied to the fee + calculations. additional_prohibitions: type: array items: type: string - description: AdditionalProhibitions is a list of additional trading prohibitions for this series. + description: >- + AdditionalProhibitions is a list of additional trading prohibitions + for this series. volume_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the total number of contracts traded across all events in this series. + description: >- + String representation of the total number of contracts traded across + all events in this series. last_updated_ts: type: string format: date-time description: Timestamp of when this series' metadata was last updated. - SeriesFeeChange: type: object required: @@ -8248,7 +8850,6 @@ components: type: string format: date-time description: Timestamp when this fee change is scheduled to take effect - GetSeriesResponse: type: object required: @@ -8256,7 +8857,6 @@ components: properties: series: $ref: '#/components/schemas/Series' - GetSeriesListResponse: type: object required: @@ -8266,7 +8866,6 @@ components: type: array items: $ref: '#/components/schemas/Series' - GetSeriesFeeChangesResponse: type: object required: @@ -8276,7 +8875,6 @@ components: type: array items: $ref: '#/components/schemas/SeriesFeeChange' - SettlementSource: type: object properties: @@ -8288,7 +8886,6 @@ components: type: string description: URL to the settlement source x-go-type-skip-optional-pointer: true - GetMarketsResponse: type: object required: @@ -8301,7 +8898,6 @@ components: $ref: '#/components/schemas/Market' cursor: type: string - GetMarketResponse: type: object required: @@ -8309,7 +8905,6 @@ components: properties: market: $ref: '#/components/schemas/Market' - MveSelectedLeg: type: object properties: @@ -8329,8 +8924,9 @@ components: $ref: '#/components/schemas/FixedPointDollars' nullable: true x-omitempty: true - description: The settlement value of the YES/LONG side of the contract in dollars. Only filled after determination - + description: >- + The settlement value of the YES/LONG side of the contract in + dollars. Only filled after determination PriceRange: type: object required: @@ -8347,7 +8943,6 @@ components: step: type: string description: Price step/tick size for this range in dollars - Market: type: object required: @@ -8393,7 +8988,9 @@ components: type: string market_type: type: string - enum: [binary, scalar] + enum: + - binary + - scalar description: Identifies the type of market title: type: string @@ -8442,11 +9039,20 @@ components: description: The amount of time after determination that the market settles status: type: string - enum: [initialized, inactive, active, closed, determined, disputed, amended, finalized] + enum: + - initialized + - inactive + - active + - closed + - determined + - disputed + - amended + - finalized description: The current status of the market in its lifecycle. response_price_units: type: string - enum: [usd_cent] + enum: + - usd_cent deprecated: true description: 'DEPRECATED: Use price_level_structure and price_ranges instead.' x-go-type-skip-optional-pointer: true @@ -8455,13 +9061,17 @@ components: description: Price for the highest YES buy offer on this market in dollars yes_bid_size_fp: $ref: '#/components/schemas/FixedPointCount' - description: Total contract size of orders to buy YES at the best bid price (fixed-point count string). + description: >- + Total contract size of orders to buy YES at the best bid price + (fixed-point count string). yes_ask_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Price for the lowest YES sell offer on this market in dollars yes_ask_size_fp: $ref: '#/components/schemas/FixedPointCount' - description: Total contract size of orders to sell YES at the best ask price (fixed-point count string). + description: >- + Total contract size of orders to sell YES at the best ask price + (fixed-point count string). no_bid_dollars: $ref: '#/components/schemas/FixedPointDollars' description: Price for the highest NO buy offer on this market in dollars @@ -8479,45 +9089,63 @@ components: description: String representation of the 24h market volume in contracts result: type: string - enum: ['yes', 'no', 'scalar', ''] + enum: + - 'yes' + - 'no' + - scalar + - '' can_close_early: type: boolean fractional_trading_enabled: type: boolean deprecated: true description: >- - Deprecated. This flag is always `true` and carries no information. Will be removed - after a pre-announcement with the removal date. + Deprecated. This flag is always `true` and carries no information. + Will be removed after a pre-announcement with the removal date. open_interest_fp: $ref: '#/components/schemas/FixedPointCount' - description: String representation of the number of contracts bought on this market disconsidering netting + description: >- + String representation of the number of contracts bought on this + market disconsidering netting notional_value_dollars: $ref: '#/components/schemas/FixedPointDollars' description: The total value of a single contract at settlement in dollars previous_yes_bid_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Price for the highest YES buy offer on this market a day ago in dollars + description: >- + Price for the highest YES buy offer on this market a day ago in + dollars previous_yes_ask_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Price for the lowest YES sell offer on this market a day ago in dollars + description: >- + Price for the lowest YES sell offer on this market a day ago in + dollars previous_price_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: Price for the last traded YES contract on this market a day ago in dollars + description: >- + Price for the last traded YES contract on this market a day ago in + dollars liquidity_dollars: $ref: '#/components/schemas/FixedPointDollars' deprecated: true - description: 'DEPRECATED: This field is deprecated and will always return "0.0000".' + description: >- + DEPRECATED: This field is deprecated and will always return + "0.0000". settlement_value_dollars: $ref: '#/components/schemas/FixedPointDollars' nullable: true x-omitempty: true - description: The settlement value of the YES/LONG side of the contract in dollars. Only filled after determination + description: >- + The settlement value of the YES/LONG side of the contract in + dollars. Only filled after determination settlement_ts: type: string format: date-time nullable: true x-omitempty: true - description: Timestamp when the market was settled. Only filled for settled markets + description: >- + Timestamp when the market was settled. Only filled for settled + markets expiration_value: type: string description: The value that was considered for the settlement @@ -8525,7 +9153,9 @@ components: type: string format: date-time nullable: true - description: The recorded datetime when the underlying event occurred, if available + description: >- + The recorded datetime when the underlying event occurred, if + available fee_waiver_expiration_time: type: string format: date-time @@ -8540,7 +9170,15 @@ components: x-go-type-skip-optional-pointer: true strike_type: type: string - enum: [greater, greater_or_equal, less, less_or_equal, between, functional, custom, structured] + enum: + - greater + - greater_or_equal + - less + - less_or_equal + - between + - functional + - custom + - structured x-omitempty: true description: Strike type defines how the market strike is defined and evaluated x-go-type-skip-optional-pointer: true @@ -8589,7 +9227,9 @@ components: x-omitempty: true price_level_structure: type: string - description: Price level structure for this market, defining price ranges and tick sizes + description: >- + Price level structure for this market, defining price ranges and + tick sizes price_ranges: type: array description: Valid price ranges for orders on this market @@ -8598,13 +9238,14 @@ components: is_provisional: type: boolean x-omitempty: true - description: If true, the market may be removed after determination if there is no activity on it + description: >- + If true, the market may be removed after determination if there is + no activity on it x-go-type-skip-optional-pointer: true exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true - tags: - name: api-keys description: API key management endpoints diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index 8aa062d..6070805 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -2828,13 +2828,13 @@ components: MarginOrdersLimitQuery: name: limit in: query - description: Number of margin orders per page. Defaults to 100. + description: Number of margin orders per page. Defaults to 10000. schema: type: integer format: int64 minimum: 1 maximum: 10000 - default: 100 + default: 10000 x-oapi-codegen-extra-tags: validate: omitempty,min=1,max=10000 MaxTsQuery: diff --git a/specs/perps_scm_openapi.yaml b/specs/perps_scm_openapi.yaml index e9f7c2c..c5810e0 100644 --- a/specs/perps_scm_openapi.yaml +++ b/specs/perps_scm_openapi.yaml @@ -422,6 +422,17 @@ components: format: date-time description: Pass as the `cursor` query param to fetch the next page. Absent when there are no more results. + MarketSettlementEstimate: + type: object + required: + - quantity_centicount + - variation_margin_centicents + - notional_value_centicents + properties: + quantity_centicount: { type: integer, format: int64, description: Position quantity in centicounts. } + variation_margin_centicents: { type: integer, format: int64, description: Variation margin for this market. } + notional_value_centicents: { type: integer, format: int64, description: Notional value for this market. } + SettlementEstimate: type: object required: @@ -436,6 +447,10 @@ components: maintenance_margin_delta_centicents: { type: integer, format: int64, description: Change in maintenance margin requirement. } maintenance_margin_required_centicents: { type: integer, format: int64, description: Maintenance margin requirement. } total_amount_centicents: { type: integer, format: int64, description: Estimated total settlement amount. } + positions: + type: object + description: Map of market ticker to that market's settlement estimate. + additionalProperties: { $ref: '#/components/schemas/MarketSettlementEstimate' } GetSettlementEstimateResponse: type: object @@ -446,6 +461,10 @@ components: type: object description: Map of subtrader ID to that subtrader's settlement estimate. additionalProperties: { $ref: '#/components/schemas/SettlementEstimate' } + prev_settlement_prices: + type: object + description: Map of market ticker to that market's most recent settlement (mark) price, in centicents. + additionalProperties: { type: integer, format: int64 } settlement_balance_centicents: { type: integer, format: int64, description: Current settlement buffer balance. } GetSettlementBalanceResponse: diff --git a/tests/_contract_support.py b/tests/_contract_support.py index d824d5f..4ff3481 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -353,22 +353,14 @@ class Exclusion: path_template="/historical/trades", ), # ── orders ────────────────────────────────────────────────────────────── - MethodEndpointEntry( - sdk_method="kalshi.resources.orders.OrdersResource.create", - http_method="POST", - path_template="/portfolio/orders", - request_body_schema="#/components/schemas/CreateOrderRequest", - ), + # V1 order-write endpoints (create/cancel/batch_create/batch_cancel/amend/ + # decrease) were removed from the spec in v3.22.0 — only the V2 family below + # remains. Reads (get/list/queue) are unchanged. MethodEndpointEntry( sdk_method="kalshi.resources.orders.OrdersResource.get", http_method="GET", path_template="/portfolio/orders/{order_id}", ), - MethodEndpointEntry( - sdk_method="kalshi.resources.orders.OrdersResource.cancel", - http_method="DELETE", - path_template="/portfolio/orders/{order_id}", - ), MethodEndpointEntry( sdk_method="kalshi.resources.orders.OrdersResource.list", http_method="GET", @@ -379,18 +371,6 @@ class Exclusion: http_method="GET", path_template="/portfolio/orders", ), - MethodEndpointEntry( - sdk_method="kalshi.resources.orders.OrdersResource.batch_create", - http_method="POST", - path_template="/portfolio/orders/batched", - request_body_schema="#/components/schemas/BatchCreateOrdersRequest", - ), - MethodEndpointEntry( - sdk_method="kalshi.resources.orders.OrdersResource.batch_cancel", - http_method="DELETE", - path_template="/portfolio/orders/batched", - request_body_schema="#/components/schemas/BatchCancelOrdersRequest", - ), MethodEndpointEntry( sdk_method="kalshi.resources.orders.OrdersResource.fills", http_method="GET", @@ -401,18 +381,6 @@ class Exclusion: http_method="GET", path_template="/portfolio/fills", ), - MethodEndpointEntry( - sdk_method="kalshi.resources.orders.OrdersResource.amend", - http_method="POST", - path_template="/portfolio/orders/{order_id}/amend", - request_body_schema="#/components/schemas/AmendOrderRequest", - ), - MethodEndpointEntry( - sdk_method="kalshi.resources.orders.OrdersResource.decrease", - http_method="POST", - path_template="/portfolio/orders/{order_id}/decrease", - request_body_schema="#/components/schemas/DecreaseOrderRequest", - ), MethodEndpointEntry( sdk_method="kalshi.resources.orders.OrdersResource.queue_positions", http_method="GET", @@ -632,6 +600,23 @@ class Exclusion: http_method="PUT", path_template="/communications/quotes/{quote_id}/confirm", ), + # RFQ-scoped quote actions (spec v3.22.0) + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.delete_for_rfq", + http_method="DELETE", + path_template="/communications/rfqs/{rfq_id}/quotes/{quote_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.accept_for_rfq", + http_method="PUT", + path_template="/communications/rfqs/{rfq_id}/quotes/{quote_id}/accept", + request_body_schema="#/components/schemas/AcceptQuoteRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.confirm_for_rfq", + http_method="PUT", + path_template="/communications/rfqs/{rfq_id}/quotes/{quote_id}/confirm", + ), # block trade proposals (openapi 3.21.0) MethodEndpointEntry( sdk_method="kalshi.resources.communications.BlockTradeProposalsResource.list", @@ -896,24 +881,6 @@ class Exclusion: # ``test_exclusion_map_is_current``), so stale entries can't silently # accumulate. EXCLUSIONS: dict[tuple[str, str], Exclusion] = { - # --- CreateOrderRequest spec fields deliberately not on the model --- - ("kalshi.models.orders.CreateOrderRequest", "yes_price"): Exclusion( - reason=( - "cent form redundant with yes_price_dollars; caller passes dollars, " - "wire carries dollars" - ), - kind="wire_normalization", - ), - ("kalshi.models.orders.CreateOrderRequest", "no_price"): Exclusion( - reason=( - "cent form redundant with no_price_dollars; caller passes dollars, wire carries dollars" - ), - kind="wire_normalization", - ), - ("kalshi.models.orders.CreateOrderRequest", "sell_position_floor"): Exclusion( - reason="deprecated in spec (only accepts 0); superseded by reduce_only", - kind="spec_deprecated", - ), # --- cursor exclusions on list_all variants (paginator-handled) --- ("kalshi.resources.markets.MarketsResource.list_all", "cursor"): Exclusion( reason="paginator-handled; not a caller-facing kwarg on list_all", @@ -983,23 +950,11 @@ class Exclusion: reason="paginator-handled; not a caller-facing kwarg on list_all", kind="paginator_handled", ), - # --- batch_cancel body param (not a query/path param) --- - ("kalshi.resources.orders.OrdersResource.batch_cancel", "orders"): Exclusion( - reason="body param (BatchCancelOrdersRequest.orders); not query/path", - kind="body_param", - ), # --- model-first overload kwarg (#56) --- - # batch_cancel is the only POST/PUT/DELETE-with-body endpoint covered by - # TestRequestParamDrift (DELETE with requestBody). The `request` kwarg is - # an SDK-side ergonomic overload, not a spec field. See #56. - # No matching AsyncOrdersResource.batch_cancel entry needed: the drift - # test indexes EXCLUSIONS by the sync FQN and reuses it for the async - # sibling (test_contracts.py: "EXCLUSIONS is indexed by the SYNC method - # fqn; async tests reuse the same allowlist entries"). - ("kalshi.resources.orders.OrdersResource.batch_cancel", "request"): Exclusion( - reason="Optional request-model overload; not a spec field. See #56.", - kind="body_param", - ), + # batch_cancel_v2 (DELETE /portfolio/events/orders/batched with a requestBody) + # is covered by TestRequestParamDrift. The `request` kwarg is an SDK-side + # ergonomic overload, not a spec field. EXCLUSIONS is indexed by the sync FQN; + # the async sibling reuses the same allowlist entry. ("kalshi.resources.orders.OrdersResource.batch_cancel_v2", "request"): Exclusion( reason=( "Required request-model kwarg for the model-only V2 surface; " @@ -1007,70 +962,6 @@ class Exclusion: ), kind="body_param", ), - # --- AmendOrderRequest spec fields deliberately not on the model --- - ("kalshi.models.orders.AmendOrderRequest", "yes_price"): Exclusion( - reason=( - "cent form redundant with yes_price_dollars; caller passes dollars, " - "wire carries dollars" - ), - kind="wire_normalization", - ), - ("kalshi.models.orders.AmendOrderRequest", "no_price"): Exclusion( - reason=( - "cent form redundant with no_price_dollars; caller passes dollars, wire carries dollars" - ), - kind="wire_normalization", - ), - # --- count wire normalization (v0.8.0) --- - # Spec has both count (int) and count_fp (FixedPointCount); SDK commits to - # emitting count_fp only (serialization_alias="count_fp"). Kalshi accepts - # either key per spec description. Documented in CHANGELOG as "count wire - # key normalized to count_fp". - ("kalshi.models.orders.CreateOrderRequest", "count"): Exclusion( - reason=( - "SDK emits count_fp (serialization_alias) instead of count; " - "Kalshi accepts either; normalized to count_fp per v0.8.0 wire shape decision" - ), - kind="wire_normalization", - ), - ("kalshi.models.orders.AmendOrderRequest", "count"): Exclusion( - reason=( - "SDK emits count_fp (serialization_alias) instead of count; " - "Kalshi accepts either; amend() used count_fp pre-v0.8.0 already" - ), - kind="wire_normalization", - ), - # --- DecreaseOrderRequest _fp variants not implemented --- - # Spec has reduce_by_fp and reduce_to_fp (FixedPointCount string) as - # alternatives to reduce_by/reduce_to (int). SDK only emits the integer - # forms. Spec says "if both provided they must match" — sending only - # integer form is valid. v0.8.0 deferred _fp variants (fractional - # contracts not yet relevant for decrease operations). - ("kalshi.models.orders.DecreaseOrderRequest", "reduce_by_fp"): Exclusion( - reason=( - "FixedPointCount variant of reduce_by; SDK emits integer reduce_by only; " - "spec accepts either form; _fp variant deferred post-v0.8.0" - ), - kind="wire_normalization", - ), - ("kalshi.models.orders.DecreaseOrderRequest", "reduce_to_fp"): Exclusion( - reason=( - "FixedPointCount variant of reduce_to; SDK emits integer reduce_to only; " - "spec accepts either form; _fp variant deferred post-v0.8.0" - ), - kind="wire_normalization", - ), - # --- BatchCancelOrdersRequest deprecated ids field --- - # Spec has ids (array of strings, marked deprecated) as an alternative to - # the preferred orders field. SDK v0.8.0 migrated from ids to orders and - # does not emit ids. Documented in CHANGELOG as BREAKING wire field flip. - ("kalshi.models.orders.BatchCancelOrdersRequest", "ids"): Exclusion( - reason=( - "deprecated spec field; SDK v0.8.0 migrated to preferred 'orders' field; " - "intentional REMOVE drift documented in CHANGELOG" - ), - kind="spec_deprecated", - ), # --- CreateOrderGroupRequest / UpdateOrderGroupLimitRequest _fp variants --- # Spec has both contracts_limit (int) and contracts_limit_fp (FixedPointCount # string) as mutually-compatible representations. SDK commits to the integer diff --git a/tests/_model_fixtures.py b/tests/_model_fixtures.py index c268bc3..5e7227d 100644 --- a/tests/_model_fixtures.py +++ b/tests/_model_fixtures.py @@ -352,25 +352,6 @@ def scope_list_dict(**overrides: Any) -> dict[str, Any]: return base -# --------------------------------------------------------------------------- -# Request bodies -# --------------------------------------------------------------------------- - - -def create_order_request_kwargs(**overrides: Any) -> dict[str, Any]: - """Kwargs for ``CreateOrderRequest(**)`` with all required fields. - - Pre-#172 ``action`` defaulted to ``"buy"``; tests now must pass it. - """ - base: dict[str, Any] = { - "ticker": "MKT-A", - "side": "yes", - "action": "buy", - } - base.update(overrides) - return base - - # --------------------------------------------------------------------------- # WebSocket payloads # --------------------------------------------------------------------------- diff --git a/tests/_request_model_fixtures.py b/tests/_request_model_fixtures.py index 8770d23..173f785 100644 --- a/tests/_request_model_fixtures.py +++ b/tests/_request_model_fixtures.py @@ -96,10 +96,9 @@ def _minimal_kwargs_no_override(model_cls: type[BaseModel]) -> dict[str, Any]: # --------------------------------------------------------------------------- _OVERRIDES: dict[str, dict[str, Any]] = { - # ``DecreaseOrderRequest`` has a ``model_validator`` requiring exactly + # ``DecreaseOrderV2Request`` has a ``model_validator`` requiring exactly # one of ``reduce_by`` / ``reduce_to``. Both fields are optional so # the auto-builder skips them — populate one. - "DecreaseOrderRequest": {"reduce_by": 1}, "DecreaseOrderV2Request": {"reduce_by": Decimal("1")}, } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 46cb5d6..e74c9e2 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -195,8 +195,8 @@ def non_marketable_price(sync_client: KalshiClient, demo_market_ticker: str) -> Uses $0.01 (1 cent) for yes side — virtually guaranteed to not fill unless the market is extremely close to $0.00. - Returns a string for compatibility with both create() (accepts str) - and CreateOrderRequest (wraps via to_decimal). + Returns a string; callers wrap it via to_decimal when building + CreateOrderV2Request.price. """ return "0.01" @@ -232,7 +232,7 @@ def cleanup_orders(sync_client: KalshiClient) -> Iterator[None]: for order in page.items: if order.client_order_id and order.client_order_id.startswith(TEST_RUN_ID): try: - sync_client.orders.cancel(order.order_id) + sync_client.orders.cancel_v2(order.order_id) logger.info("Cleanup: cancelled order %s", order.order_id) except Exception: logger.warning("Cleanup: failed to cancel order %s", order.order_id) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index c208a41..21b56d9 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -15,6 +15,7 @@ from kalshi.client import KalshiClient from kalshi.errors import KalshiConnectionError, KalshiError, KalshiNotFoundError +from kalshi.models.orders import CreateOrderV2Request def wait_for_resource[T]( @@ -150,11 +151,11 @@ def fill_guarantee( test_run_id: str, price: str = "0.50", ) -> tuple[str, str]: - """Place opposing buy+sell orders to produce a fill. + """Place opposing bid+ask orders to produce a fill. - Places a YES buy and YES sell at the same price (both count=1). + Places a YES bid and YES ask at the same price (both count=1). If the orderbook has liquidity, uses the midpoint. Otherwise falls - back to the provided price (default $0.50). Returns (buy_order_id, sell_order_id). + back to the provided price (default $0.50). Returns (bid_order_id, ask_order_id). Skips the test if either order is rejected (e.g., self-trade prohibited). @@ -172,38 +173,44 @@ def fill_guarantee( if Decimal("0.01") <= midpoint <= Decimal("0.99"): price = str(midpoint) - # Place buy order + # Place bid order try: - buy_order = client.orders.create( - ticker=ticker, - side="yes", - action="buy", - count=1, - yes_price=price, - client_order_id=f"{test_run_id}-fill-buy", + buy_order = client.orders.create_v2( + request=CreateOrderV2Request( + ticker=ticker, + side="bid", + count=Decimal("1"), + price=Decimal(price), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + client_order_id=f"{test_run_id}-fill-buy", + ) ) except KalshiError as exc: - pytest.skip(f"Buy order rejected: {exc}") + pytest.skip(f"Bid order rejected: {exc}") - # Place sell order to match against the buy + # Place ask order to match against the bid try: - sell_order = client.orders.create( - ticker=ticker, - side="yes", - action="sell", - count=1, - yes_price=price, - client_order_id=f"{test_run_id}-fill-sell", + sell_order = client.orders.create_v2( + request=CreateOrderV2Request( + ticker=ticker, + side="ask", + count=Decimal("1"), + price=Decimal(price), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + client_order_id=f"{test_run_id}-fill-sell", + ) ) except KalshiError as exc: - # Clean up the resting buy order + # Clean up the resting bid order try: - client.orders.cancel(buy_order.order_id) + client.orders.cancel_v2(buy_order.order_id) except KalshiError: logger.warning( - "Failed to cancel buy order %s during fill_guarantee cleanup", + "Failed to cancel bid order %s during fill_guarantee cleanup", buy_order.order_id, ) - pytest.skip(f"Sell order rejected (self-trade prohibited?): {exc}") + pytest.skip(f"Ask order rejected (self-trade prohibited?): {exc}") return buy_order.order_id, sell_order.order_id diff --git a/tests/integration/test_orders.py b/tests/integration/test_orders.py index d27dafc..846a974 100644 --- a/tests/integration/test_orders.py +++ b/tests/integration/test_orders.py @@ -2,37 +2,23 @@ from __future__ import annotations -import logging -import time - import pytest from kalshi.async_client import AsyncKalshiClient from kalshi.client import KalshiClient from kalshi.models.common import Page -from kalshi.models.orders import CreateOrderRequest, Fill, Order -from kalshi.types import to_decimal +from kalshi.models.orders import Fill, Order from tests.integration.assertions import assert_model_fields -from tests.integration.conftest import skip_if_low_balance from tests.integration.coverage_harness import register -from tests.integration.helpers import await_resource, fill_guarantee, wait_for_resource - -logger = logging.getLogger(__name__) register( "OrdersResource", [ - "amend", "amend_v2", - "batch_cancel", "batch_cancel_v2", - "batch_create", "batch_create_v2", - "cancel", "cancel_v2", - "create", "create_v2", - "decrease", "decrease_v2", "fills", "fills_all", @@ -77,141 +63,6 @@ def test_fills_all(self, sync_client: KalshiClient) -> None: if count >= 2: break - def test_order_fill_lifecycle( - self, - sync_client: KalshiClient, - demo_market_ticker: str, - demo_balance_cents: int, - test_run_id: str, - ) -> None: - """Attempt to produce a fill via opposing orders, verify fill data. - - On demo, self-trading is blocked (the sell side gets canceled). - When fills exist (from prior trading or a multi-account setup), - this test verifies the full lifecycle. Otherwise it verifies that - opposing orders are placed and cleaned up correctly. - """ - skip_if_low_balance(demo_balance_cents, threshold_cents=2000) - - buy_id, sell_id = fill_guarantee( - sync_client, demo_market_ticker, test_run_id=test_run_id, - ) - - # Check order statuses — on demo, self-trade prevention may - # cancel one side immediately. Demo's query-exchange replica - # lags writes by ~10s, so poll for visibility. - buy_order = wait_for_resource(lambda: sync_client.orders.get(buy_id)) - sell_order = wait_for_resource(lambda: sync_client.orders.get(sell_id)) - assert_model_fields(buy_order) - assert_model_fields(sell_order) - - # Poll for fills (demo server may need time to propagate) - our_fills = [] - for _ in range(3): - time.sleep(0.5) - page = sync_client.orders.fills(limit=20) - our_fills = [ - f for f in page.items - if f.order_id in (buy_id, sell_id) - ] - if our_fills: - break - - if our_fills: - fill = our_fills[0] - assert isinstance(fill, Fill) - assert_model_fields(fill) - # ticker is canonical; market_ticker is the legacy alias (per OpenAPI spec) - assert fill.ticker == demo_market_ticker - assert fill.yes_price is not None - assert fill.count is not None - assert fill.created_time is not None - assert fill.side in ("yes", "no") - else: - # Self-trading blocked on demo — verify orders were placed - # and have expected statuses - assert buy_order.order_id == buy_id - assert sell_order.order_id == sell_id - - def test_create_get_cancel( - self, - sync_client: KalshiClient, - demo_market_ticker: str, - non_marketable_price: str, - demo_balance_cents: int, - test_run_id: str, - ) -> None: - """Create an order, retrieve it, then cancel it.""" - skip_if_low_balance(demo_balance_cents) - client_order_id = f"{test_run_id}-create" - - order = sync_client.orders.create( - ticker=demo_market_ticker, - side="yes", - action="buy", - count=1, - yes_price=non_marketable_price, - client_order_id=client_order_id, - ) - assert isinstance(order, Order) - assert_model_fields(order) - assert order.order_id - - try: - retrieved = wait_for_resource( - lambda: sync_client.orders.get(order.order_id), - ) - assert isinstance(retrieved, Order) - assert_model_fields(retrieved) - assert retrieved.order_id == order.order_id - finally: - try: - sync_client.orders.cancel(order.order_id) - except Exception: - logger.warning("Failed to cancel order %s in teardown", order.order_id) - - def test_batch_create_cancel( - self, - sync_client: KalshiClient, - demo_market_ticker: str, - non_marketable_price: str, - demo_balance_cents: int, - test_run_id: str, - ) -> None: - """Batch create orders, then batch cancel them.""" - skip_if_low_balance(demo_balance_cents) - - requests = [ - CreateOrderRequest( - ticker=demo_market_ticker, - side="yes", - action="buy", - count=to_decimal(1), - yes_price=to_decimal(non_marketable_price), - client_order_id=f"{test_run_id}-batch-{i}", - ) - for i in range(2) - ] - - response = sync_client.orders.batch_create(requests) - assert len(response.orders) > 0 - successful = [e.order for e in response.orders if e.order is not None] - assert successful, f"All legs failed: {response.orders}" - for o in successful: - assert isinstance(o, Order) - assert_model_fields(o) - - order_ids = [o.order_id for o in successful] - try: - sync_client.orders.batch_cancel(order_ids) - except Exception: - # Clean up individually if batch cancel fails - for oid in order_ids: - try: - sync_client.orders.cancel(oid) - except Exception: - logger.warning("Failed to cancel order %s in batch teardown", oid) - @pytest.mark.integration class TestOrdersAsync: @@ -246,81 +97,3 @@ async def test_fills_all(self, async_client: AsyncKalshiClient) -> None: count += 1 if count >= 3: break - - async def test_create_get_cancel( - self, - async_client: AsyncKalshiClient, - demo_market_ticker: str, - non_marketable_price: str, - demo_balance_cents: int, - test_run_id: str, - ) -> None: - """Async: create, retrieve, cancel.""" - skip_if_low_balance(demo_balance_cents) - client_order_id = f"{test_run_id}-async-create" - - order = await async_client.orders.create( - ticker=demo_market_ticker, - side="yes", - action="buy", - count=1, - yes_price=non_marketable_price, - client_order_id=client_order_id, - ) - assert isinstance(order, Order) - assert_model_fields(order) - assert order.order_id - - try: - retrieved = await await_resource( - lambda: async_client.orders.get(order.order_id), - ) - assert isinstance(retrieved, Order) - assert_model_fields(retrieved) - assert retrieved.order_id == order.order_id - finally: - try: - await async_client.orders.cancel(order.order_id) - except Exception: - logger.warning("Failed to cancel async order %s", order.order_id) - - async def test_batch_create_cancel( - self, - async_client: AsyncKalshiClient, - demo_market_ticker: str, - non_marketable_price: str, - demo_balance_cents: int, - test_run_id: str, - ) -> None: - """Async: batch create, then batch cancel.""" - skip_if_low_balance(demo_balance_cents) - - requests = [ - CreateOrderRequest( - ticker=demo_market_ticker, - side="yes", - action="buy", - count=to_decimal(1), - yes_price=to_decimal(non_marketable_price), - client_order_id=f"{test_run_id}-async-batch-{i}", - ) - for i in range(2) - ] - - response = await async_client.orders.batch_create(requests) - assert len(response.orders) > 0 - successful = [e.order for e in response.orders if e.order is not None] - assert successful, f"All legs failed: {response.orders}" - for o in successful: - assert isinstance(o, Order) - assert_model_fields(o) - - order_ids = [o.order_id for o in successful] - try: - await async_client.orders.batch_cancel(order_ids) - except Exception: - for oid in order_ids: - try: - await async_client.orders.cancel(oid) - except Exception: - logger.warning("Failed to cancel async order %s", oid) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index ac8612a..0100ee0 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -611,18 +611,29 @@ def test_from_env_typed_kwargs_forwarded(self, monkeypatch: pytest.MonkeyPatch) class TestAsyncUnauthenticatedResourceGuards: @pytest.mark.asyncio - async def test_orders_create_raises_auth_required(self) -> None: + async def test_orders_create_v2_raises_auth_required(self) -> None: config = KalshiConfig( base_url="https://test.kalshi.com/trade-api/v2", timeout=5.0, max_retries=0, ) transport = AsyncTransport(None, config) + from kalshi.models.orders import CreateOrderV2Request from kalshi.resources.orders import AsyncOrdersResource resource = AsyncOrdersResource(transport) with pytest.raises(AuthRequiredError): - await resource.create(ticker="TEST", side="yes", action="buy", count=1) + await resource.create_v2( + request=CreateOrderV2Request( + ticker="TEST", + client_order_id="c-1", + side="bid", + count="1", + price="0.50", + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) + ) @pytest.mark.asyncio async def test_portfolio_balance_raises_auth_required(self) -> None: diff --git a/tests/test_async_orders.py b/tests/test_async_orders.py index 1537a3a..82b29d6 100644 --- a/tests/test_async_orders.py +++ b/tests/test_async_orders.py @@ -11,22 +11,18 @@ import respx from kalshi._base_client import AsyncTransport -from kalshi.async_client import AsyncKalshiClient from kalshi.auth import KalshiAuth from kalshi.config import KalshiConfig from kalshi.errors import ( AuthRequiredError, KalshiError, KalshiNotFoundError, - KalshiValidationError, ) from kalshi.models.orders import ( - AmendOrderResponse, AmendOrderV2Request, BatchCancelOrdersV2Request, BatchCancelOrdersV2RequestOrder, BatchCreateOrdersV2Request, - CreateOrderRequest, CreateOrderV2Request, DecreaseOrderV2Request, ) @@ -53,308 +49,6 @@ def unauth_orders_async(config: KalshiConfig) -> AsyncOrdersResource: return AsyncOrdersResource(AsyncTransport(None, config)) -@pytest.fixture -def client(test_auth: KalshiAuth) -> AsyncKalshiClient: - """AsyncKalshiClient wired to the demo base URL (matches wire-shape test mocks).""" - from kalshi.config import DEMO_BASE_URL, DEMO_WS_URL - - cfg = KalshiConfig(base_url=DEMO_BASE_URL, ws_base_url=DEMO_WS_URL, timeout=5.0, max_retries=0) - return AsyncKalshiClient(auth=test_auth, config=cfg) - - -_MINIMAL_ORDER = order_dict(order_id="ord-123", ticker="MKT", side="yes", status="resting") - - -class TestCreateOrderWireShapeAsync: - """v0.8.0: async orders.create() builds CreateOrderRequest internally and - serializes via model_dump. Wire body must not contain phantom `type` - field; count must serialize as count_fp; new fields reach the wire.""" - - @respx.mock - @pytest.mark.asyncio - async def test_no_phantom_type_in_wire( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - await client.orders.create(ticker="MKT", side="yes", action="buy", count=1, yes_price=0.5) - - body = json.loads(route.calls[0].request.content) - assert "type" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_count_fp_not_count_in_wire( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - await client.orders.create(ticker="MKT", side="yes", action="buy", yes_price=0.5, count=3) - - body = json.loads(route.calls[0].request.content) - assert "count_fp" in body - assert body["count_fp"] == "3" - assert "count" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_time_in_force_reaches_wire( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - await client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - time_in_force="fill_or_kill", - ) - - body = json.loads(route.calls[0].request.content) - assert body["time_in_force"] == "fill_or_kill" - - @respx.mock - @pytest.mark.asyncio - async def test_post_only_reduce_only_reach_wire( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - await client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - post_only=True, - reduce_only=False, - ) - - body = json.loads(route.calls[0].request.content) - assert body["post_only"] is True - assert body["reduce_only"] is False - - @respx.mock - @pytest.mark.asyncio - async def test_buy_max_cost_int_cents_wire( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """Spec says cents. SDK must send int on the wire.""" - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - await client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - buy_max_cost=500, - ) - - body = json.loads(route.calls[0].request.content) - assert body["buy_max_cost"] == 500 - assert isinstance(body["buy_max_cost"], int) - - @respx.mock - @pytest.mark.asyncio - async def test_subaccount_order_group_cancel_on_pause_stp_wire( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - await client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - subaccount=2, - order_group_id="grp-x", - cancel_order_on_pause=True, - self_trade_prevention_type="maker", - ) - - body = json.loads(route.calls[0].request.content) - assert body["subaccount"] == 2 - assert body["order_group_id"] == "grp-x" - assert body["cancel_order_on_pause"] is True - assert body["self_trade_prevention_type"] == "maker" - - @pytest.mark.asyncio - async def test_type_kwarg_removed(self, client: AsyncKalshiClient) -> None: - """v0.8.0 removed the `type` kwarg from orders.create().""" - with pytest.raises(TypeError): - await client.orders.create( - ticker="MKT", - side="yes", - type="market", # type: ignore[call-arg] - ) - - @respx.mock - @pytest.mark.asyncio - async def test_missing_count_and_action_raises_before_http( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """#242: async kwarg-form create() with missing ``count`` / ``action`` - must raise ``TypeError`` BEFORE any HTTP request is dispatched.""" - route = respx_mock.post( - "https://demo-api.kalshi.co/trade-api/v2/portfolio/orders" - ).mock(return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER})) - - with pytest.raises(TypeError, match=r"count.*action"): - await client.orders.create(ticker="X", side="yes") - with pytest.raises(TypeError, match=r"count.*action"): - await client.orders.create(ticker="X", side="yes", action="buy") - with pytest.raises(TypeError, match=r"count.*action"): - await client.orders.create(ticker="X", side="yes", count=10) - - assert route.call_count == 0 - - @respx.mock - @pytest.mark.asyncio - async def test_explicit_count_action_kwargs_still_work( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """#242: regression — explicit ``count`` + ``action`` kwargs still build - and dispatch normally.""" - route = respx_mock.post( - "https://demo-api.kalshi.co/trade-api/v2/portfolio/orders" - ).mock(return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER})) - - await client.orders.create( - ticker="X", side="yes", count=10, action="buy", yes_price="0.5", - ) - - assert route.call_count == 1 - body = json.loads(route.calls[0].request.content) - assert body["count_fp"] == "10" - assert body["action"] == "buy" - assert body["yes_price_dollars"] == "0.5" - - @respx.mock - @pytest.mark.asyncio - async def test_request_overload_unaffected_by_242( - self, - client: AsyncKalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """#242: ``request=CreateOrderRequest(...)`` path unaffected by the - kwarg-overload guard.""" - route = respx_mock.post( - "https://demo-api.kalshi.co/trade-api/v2/portfolio/orders" - ).mock(return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER})) - - await client.orders.create( - request=CreateOrderRequest( - ticker="X", - side="yes", - count=Decimal("10"), - action="buy", - yes_price=Decimal("0.5"), - ) - ) - - assert route.call_count == 1 - body = json.loads(route.calls[0].request.content) - assert body["count_fp"] == "10" - assert body["action"] == "buy" - - -class TestAsyncOrdersCreate: - @respx.mock - @pytest.mark.asyncio - async def test_create_limit_order(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-123", - ticker="TEST-MKT", - side="yes", - status="resting", - yes_price_dollars="0.6500", - count_fp="10", - ) - }, - ) - ) - order = await orders.create( - ticker="TEST-MKT", side="yes", action="buy", count=10, yes_price=0.65 - ) - assert order.order_id == "ord-123" - assert order.yes_price == Decimal("0.6500") - assert order.count == 10 - - @respx.mock - @pytest.mark.asyncio - async def test_create_order_no_price(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict(order_id="ord-456", ticker="TEST-MKT", status="executed") - }, - ) - ) - order = await orders.create(ticker="TEST-MKT", side="yes", action="buy", count=1) - assert order.order_id == "ord-456" - - @respx.mock - @pytest.mark.asyncio - async def test_decimal_price_conversion(self, orders: AsyncOrdersResource) -> None: - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response( - 200, - json={"order": order_dict(order_id="ord-789", ticker="T")}, - ) - ) - await orders.create(ticker="T", side="yes", action="buy", count=1, yes_price=0.65) - - body = json.loads(route.calls[0].request.content) - assert body["yes_price_dollars"] == "0.65" - assert "yes_price" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_validation_error(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(400, json={"message": "invalid ticker"}) - ) - with pytest.raises(KalshiValidationError): - await orders.create(ticker="INVALID", side="yes", action="buy", count=1) - - class TestAsyncOrdersGet: @respx.mock @pytest.mark.asyncio @@ -379,26 +73,6 @@ async def test_not_found(self, orders: AsyncOrdersResource) -> None: await orders.get("fake") -class TestAsyncOrdersCancel: - @respx.mock - @pytest.mark.asyncio - async def test_cancel_order(self, orders: AsyncOrdersResource) -> None: - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123").mock( - return_value=httpx.Response(200, json={}) - ) - await orders.cancel("ord-123") # should not raise - - @respx.mock - @pytest.mark.asyncio - async def test_cancel_with_subaccount(self, orders: AsyncOrdersResource) -> None: - route = respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-456").mock( - return_value=httpx.Response(200, json={}) - ) - await orders.cancel("ord-456", subaccount=42) - params = dict(route.calls[0].request.url.params) - assert params["subaccount"] == "42" - - class TestAsyncOrdersList: @respx.mock @pytest.mark.asyncio @@ -543,126 +217,6 @@ async def test_list_all_with_all_new_filters(self, orders: AsyncOrdersResource) assert params["subaccount"] == "7" -class TestAsyncOrdersBatch: - @respx.mock - @pytest.mark.asyncio - async def test_batch_create(self, orders: AsyncOrdersResource) -> None: - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order": order_dict(order_id="b1", ticker="A"), - "error": None, - "client_order_id": "c1", - }, - { - "order": order_dict(order_id="b2", ticker="B"), - "error": None, - "client_order_id": "c2", - }, - ] - }, - ) - ) - reqs = [ - CreateOrderRequest(ticker="A", side="yes", action="buy", count=1), - CreateOrderRequest(ticker="B", side="no", action="buy", count=1), - ] - from kalshi.models.orders import BatchCreateOrdersResponse - - result = await orders.batch_create(reqs) - assert isinstance(result, BatchCreateOrdersResponse) - assert len(result.orders) == 2 - assert result.orders[0].order is not None - assert result.orders[0].order.order_id == "b1" - assert result.orders[0].client_order_id == "c1" - - # Verify serialization uses _dollars alias - body = json.loads(route.calls[0].request.content) - for order_body in body["orders"]: - assert "yes_price" not in order_body - - @respx.mock - @pytest.mark.asyncio - async def test_batch_create_partial_failure_does_not_crash( - self, orders: AsyncOrdersResource - ) -> None: - """Async mirror of #194 partial-failure regression.""" - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order": order_dict(order_id="ok", ticker="A"), - "error": None, - "client_order_id": "good", - }, - { - "order": None, - "error": {"code": "bad_price", "message": "rejected"}, - "client_order_id": "bad", - }, - ] - }, - ) - ) - result = await orders.batch_create( - [CreateOrderRequest(ticker="A", side="yes", action="buy", count=1)] - ) - assert result.orders[0].order is not None - assert result.orders[1].order is None - assert result.orders[1].error is not None - assert result.orders[1].error["code"] == "bad_price" - - @respx.mock - @pytest.mark.asyncio - async def test_batch_cancel(self, orders: AsyncOrdersResource) -> None: - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order_id": "o1", - "reduced_by_fp": "2", - "order": None, - "error": None, - }, - { - "order_id": "o2", - "reduced_by_fp": "0", - "order": None, - "error": {"code": "not_found", "message": "gone"}, - }, - ] - }, - ) - ) - from kalshi.models.orders import BatchCancelOrdersResponse - - result = await orders.batch_cancel(["o1", "o2"]) - assert isinstance(result, BatchCancelOrdersResponse) - assert result.orders[0].reduced_by_fp == Decimal("2") - assert result.orders[1].error is not None - assert result.orders[1].error["code"] == "not_found" - - @respx.mock - @pytest.mark.asyncio - async def test_batch_cancel_raises_on_204( - self, orders: AsyncOrdersResource - ) -> None: - from kalshi.errors import KalshiError - - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(204) - ) - with pytest.raises(KalshiError, match="Expected BatchCancelOrdersResponse"): - await orders.batch_cancel(["ord-1"]) - - class TestAsyncOrdersFills: @respx.mock @pytest.mark.asyncio @@ -776,128 +330,6 @@ async def test_fills_all_with_all_new_filters(self, orders: AsyncOrdersResource) assert params["subaccount"] == "7" -class TestAsyncOrdersAmend: - @respx.mock - @pytest.mark.asyncio - async def test_amend_price(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123/amend").mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict( - order_id="ord-123", ticker="T", yes_price_dollars="0.5000" - ), - "order": order_dict(order_id="ord-456", ticker="T", yes_price_dollars="0.6500"), - }, - ) - ) - result = await orders.amend("ord-123", ticker="T", side="yes", action="buy", yes_price=0.65) - assert isinstance(result, AmendOrderResponse) - assert result.order.yes_price == Decimal("0.6500") - - @respx.mock - @pytest.mark.asyncio - async def test_amend_not_found(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/fake/amend").mock( - return_value=httpx.Response(404, json={"message": "not found"}) - ) - with pytest.raises(KalshiNotFoundError): - await orders.amend("fake", ticker="T", side="yes", action="buy", yes_price=0.50) - - @respx.mock - @pytest.mark.asyncio - async def test_amend_serializes_dollars_and_count(self, orders: AsyncOrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-123", ticker="T"), - "order": order_dict(order_id="ord-123", ticker="T"), - }, - ) - ) - await orders.amend( - "ord-123", ticker="T", side="yes", action="buy", yes_price=0.55, count=20 - ) - body = json.loads(route.calls[0].request.content) - assert body["yes_price_dollars"] == "0.55" - assert body["count_fp"] == "20" - assert "yes_price" not in body - assert "count" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_amend_validation_error(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123/amend").mock( - return_value=httpx.Response(400, json={"message": "invalid"}) - ) - with pytest.raises(KalshiValidationError): - await orders.amend("ord-123", ticker="T", side="yes", action="buy", yes_price=0.50) - - @pytest.mark.asyncio - async def test_amend_requires_price_or_count(self, orders: AsyncOrdersResource) -> None: - with pytest.raises(ValueError, match="requires at least one"): - await orders.amend("ord-123", ticker="T", side="yes", action="buy") - - -class TestAsyncOrdersDecrease: - @respx.mock - @pytest.mark.asyncio - async def test_decrease_by(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123/decrease").mock( - return_value=httpx.Response( - 200, - json={"order": order_dict(order_id="ord-123", remaining_count_fp="5")}, - ) - ) - order = await orders.decrease("ord-123", reduce_by=5) - assert order.remaining_count == Decimal("5") - - @respx.mock - @pytest.mark.asyncio - async def test_decrease_validation_error(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123/decrease").mock( - return_value=httpx.Response(400, json={"message": "invalid"}) - ) - with pytest.raises(KalshiValidationError): - await orders.decrease("ord-123", reduce_by=-1) - - @respx.mock - @pytest.mark.asyncio - async def test_decrease_to(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123/decrease").mock( - return_value=httpx.Response( - 200, - json={"order": order_dict(order_id="ord-123", remaining_count_fp="0")}, - ) - ) - order = await orders.decrease("ord-123", reduce_to=0) - assert order.remaining_count == Decimal("0") - - @respx.mock - @pytest.mark.asyncio - async def test_decrease_not_found(self, orders: AsyncOrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/fake/decrease").mock( - return_value=httpx.Response(404, json={"message": "not found"}) - ) - with pytest.raises(KalshiNotFoundError): - await orders.decrease("fake", reduce_by=1) - - @pytest.mark.asyncio - async def test_decrease_requires_reduce_arg(self, orders: AsyncOrdersResource) -> None: - with pytest.raises(ValueError, match="requires either reduce_by or reduce_to"): - await orders.decrease("ord-123") - - @pytest.mark.asyncio - async def test_decrease_rejects_both_reduce_args(self, orders: AsyncOrdersResource) -> None: - with pytest.raises(ValueError, match="not both"): - await orders.decrease("ord-123", reduce_by=5, reduce_to=3) - - class TestAsyncOrdersQueuePositions: @respx.mock @pytest.mark.asyncio @@ -972,20 +404,6 @@ async def test_queue_position_missing_key_raises(self, orders: AsyncOrdersResour class TestAsyncOrdersAuthGuards: - @pytest.mark.asyncio - async def test_amend_requires_auth(self, unauth_orders_async: AsyncOrdersResource) -> None: - from kalshi.errors import AuthRequiredError - - with pytest.raises(AuthRequiredError): - await unauth_orders_async.amend("ord-123", ticker="T", side="yes", action="buy") - - @pytest.mark.asyncio - async def test_decrease_requires_auth(self, unauth_orders_async: AsyncOrdersResource) -> None: - from kalshi.errors import AuthRequiredError - - with pytest.raises(AuthRequiredError): - await unauth_orders_async.decrease("ord-123", reduce_by=1) - @pytest.mark.asyncio async def test_queue_positions_requires_auth( self, @@ -1007,280 +425,6 @@ async def test_queue_position_requires_auth( await unauth_orders_async.queue_position("ord-123") -class TestBatchCancelWireShapeAsync: - @respx.mock - @pytest.mark.asyncio - async def test_wraps_str_ids_into_orders(self, orders: AsyncOrdersResource) -> None: - route = respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - await orders.batch_cancel(["ord-1", "ord-2"]) - - body = json.loads(route.calls[0].request.content) - assert "ids" not in body # deprecated field no longer used - assert "orders" in body - assert body["orders"] == [ - {"order_id": "ord-1"}, - {"order_id": "ord-2"}, - ] - - @respx.mock - @pytest.mark.asyncio - async def test_accepts_typed_order_entries(self, orders: AsyncOrdersResource) -> None: - from kalshi.models.orders import BatchCancelOrdersRequestOrder - - route = respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - await orders.batch_cancel( - [ - BatchCancelOrdersRequestOrder(order_id="ord-1", subaccount=5), - BatchCancelOrdersRequestOrder(order_id="ord-2"), - ] - ) - - body = json.loads(route.calls[0].request.content) - assert body["orders"] == [ - {"order_id": "ord-1", "subaccount": 5}, - {"order_id": "ord-2"}, - ] - - -class TestAsyncBatchCancelRoutesThroughDeleteWithBodyJson: - """Regression for issue #47 / #223: async batch_cancel must route through - the shared ``AsyncResource._delete_with_body_json`` bytes helper so any - future retry / error-mapping behavior added to the helper applies to the - async path. Sync and async paths must stay symmetric. - """ - - @respx.mock - @pytest.mark.asyncio - async def test_batch_cancel_uses_delete_with_body_json_helper( - self, orders: AsyncOrdersResource, - ) -> None: - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - with patch.object( - orders, - "_delete_with_body_json", - wraps=orders._delete_with_body_json, - new_callable=AsyncMock, - ) as spy: - await orders.batch_cancel(["ord-1", "ord-2"]) - spy.assert_called_once() - args, kwargs = spy.call_args - assert args == ("/portfolio/orders/batched",) - # Bytes path: caller passes pre-serialized JSON via ``content=``, - # NOT a dict via ``json=`` (P4.2). - assert "json" not in kwargs - assert isinstance(kwargs["content"], bytes) - assert json.loads(kwargs["content"]) == { - "orders": [{"order_id": "ord-1"}, {"order_id": "ord-2"}] - } - - -class TestAmendWireShapeAsync: - """v0.8.0: async orders.amend() builds AmendOrderRequest internally and - serializes via model_dump. Price fields must use _dollars suffix; - count must use count_fp alias; phantom keys must be absent.""" - - @respx.mock - @pytest.mark.asyncio - async def test_price_serializes_dollars_alias(self, orders: AsyncOrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - await orders.amend( - "ord-99", - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", - ) - - body = json.loads(route.calls[0].request.content) - assert body["yes_price_dollars"] == "0.55" - assert "yes_price" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_count_serializes_fp_alias(self, orders: AsyncOrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - await orders.amend( - "ord-99", - ticker="MKT", - side="yes", - action="buy", - count=3, - ) - - body = json.loads(route.calls[0].request.content) - assert body["count_fp"] == "3" - assert "count" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_required_and_optional_fields(self, orders: AsyncOrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - await orders.amend( - "ord-99", - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", - count=3, - subaccount=2, - client_order_id="c-old", - updated_client_order_id="c-new", - ) - - body = json.loads(route.calls[0].request.content) - assert body["ticker"] == "MKT" - assert body["side"] == "yes" - assert body["action"] == "buy" - assert body["yes_price_dollars"] == "0.55" - assert body["count_fp"] == "3" - assert body["subaccount"] == 2 - assert body["client_order_id"] == "c-old" - assert body["updated_client_order_id"] == "c-new" - assert "yes_price" not in body - assert "no_price" not in body - assert "count" not in body - - @respx.mock - @pytest.mark.asyncio - async def test_no_price_absent_when_not_passed(self, orders: AsyncOrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - await orders.amend( - "ord-99", - ticker="MKT", - side="no", - action="buy", - no_price="0.45", - ) - - body = json.loads(route.calls[0].request.content) - assert body["no_price_dollars"] == "0.45" - assert "no_price" not in body - assert "yes_price_dollars" not in body - assert "count_fp" not in body - - -class TestDecreaseWireShapeAsync: - """v0.8.0: async orders.decrease() builds DecreaseOrderRequest internally.""" - - @respx.mock - @pytest.mark.asyncio - async def test_reduce_by_body(self, orders: AsyncOrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/decrease" - ).mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-99", ticker="MKT", side="yes", status="resting" - ), - }, - ) - ) - - await orders.decrease("ord-99", reduce_by=5, subaccount=1) - - body = json.loads(route.calls[0].request.content) - assert body == {"reduce_by": 5, "subaccount": 1} - - @respx.mock - @pytest.mark.asyncio - async def test_reduce_to_body(self, orders: AsyncOrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/decrease" - ).mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-99", ticker="MKT", side="yes", status="resting" - ), - }, - ) - ) - - await orders.decrease("ord-99", reduce_to=2) - - body = json.loads(route.calls[0].request.content) - assert body == {"reduce_to": 2} - - -class TestBatchCreateWireShapeAsync: - """v0.8.0: async orders.batch_create() wraps via BatchCreateOrdersRequest.""" - - @respx.mock - @pytest.mark.asyncio - async def test_wraps_orders_key(self, orders: AsyncOrdersResource) -> None: - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - await orders.batch_create( - [ - CreateOrderRequest(ticker="A", side="yes", action="buy", count=1), - CreateOrderRequest(ticker="B", side="no", action="buy", count=1), - ] - ) - - body = json.loads(route.calls[0].request.content) - assert "orders" in body - assert len(body["orders"]) == 2 - assert set(body.keys()) == {"orders"} - - # ── V2 event-market orders (spec v3.18.0) ─────────────────── diff --git a/tests/test_client.py b/tests/test_client.py index 95676ed..b043524 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -995,19 +995,6 @@ def test_carries_channel_op(self) -> None: class TestUnauthenticatedResourceGuards: - def test_orders_create_raises_auth_required(self) -> None: - config = KalshiConfig( - base_url="https://test.kalshi.com/trade-api/v2", - timeout=5.0, - max_retries=0, - ) - transport = SyncTransport(None, config) - from kalshi.resources.orders import OrdersResource - - resource = OrdersResource(transport) - with pytest.raises(AuthRequiredError): - resource.create(ticker="TEST", side="yes", action="buy", count=1) - def test_orders_list_raises_auth_required(self) -> None: config = KalshiConfig( base_url="https://test.kalshi.com/trade-api/v2", @@ -1681,53 +1668,3 @@ def test_sync_transport_close_is_idempotent(self, transport: SyncTransport) -> N transport.close() transport.close() # triple-close OK assert transport._closed is True - - -class TestIssue350OrdersCreateOverloadRequiresActionCount: - """#350: orders.create() kwarg overload requires ``action`` and ``count``. - - v2.5 (#242) removed the silent ``count=1`` / ``action="buy"`` defaults - at runtime, raising ``TypeError`` when either is missing. The kwarg - overload still advertised them as ``... | None = ...``, so mypy - accepted the missing-arg shape silently. This test pins both the - runtime guard and the type-system fence: removing ``action`` or - ``count`` triggers a ``call-overload`` mypy error and a runtime - ``TypeError`` before any HTTP traffic. - """ - - def test_issue_350_orders_create_overload_requires_action_count( - self, test_auth: KalshiAuth - ) -> None: - config = KalshiConfig( - base_url="https://test.kalshi.com/trade-api/v2", - timeout=5.0, - max_retries=0, - ) - transport = SyncTransport(test_auth, config) - from kalshi.resources.orders import OrdersResource - - resource = OrdersResource(transport) - - # Runtime guard from #242: missing ``action`` raises before HTTP. - with pytest.raises(TypeError, match=r"action"): - resource.create( # type: ignore[call-overload] - ticker="TEST", - side="yes", - count=1, - ) - - # Runtime guard: missing ``count`` raises before HTTP. - with pytest.raises(TypeError, match=r"count"): - resource.create( # type: ignore[call-overload] - ticker="TEST", - side="yes", - action="buy", - ) - - # The ``# type: ignore[call-overload]`` markers above demonstrate the - # static fence: mypy --strict refuses the missing-arg shapes; if the - # overload were re-loosened to ``ActionLiteral | None = ...`` again, - # mypy would emit ``unused-ignore`` on these lines and fail the - # repo-wide strict check. - - transport.close() diff --git a/tests/test_communications.py b/tests/test_communications.py index 64efc6f..16b1ba5 100644 --- a/tests/test_communications.py +++ b/tests/test_communications.py @@ -197,7 +197,7 @@ def test_create_rfq_rejects_zero_contracts(self) -> None: CreateRFQRequest(market_ticker="MKT-1", rest_remainder=True, contracts=0) def test_create_quote_request_serializes_bids_without_dollars_suffix(self) -> None: - # Unlike CreateOrderRequest, spec wire uses yes_bid / no_bid for this one. + # Spec wire uses yes_bid / no_bid (no _dollars suffix) for this one. req = CreateQuoteRequest( rfq_id="rfq-1", yes_bid=Decimal("0.56"), @@ -615,6 +615,47 @@ def test_404(self, comms: CommunicationsResource) -> None: comms.confirm_quote("gone") +class TestRFQScopedQuoteActions: + """RFQ-scoped quote actions (spec v3.22.0): + ``/communications/rfqs/{rfq_id}/quotes/{quote_id}[/accept|/confirm]``.""" + + @respx.mock + def test_delete_for_rfq_sends_delete(self, comms: CommunicationsResource) -> None: + route = respx.delete( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/q-1", + ).mock(return_value=httpx.Response(204)) + comms.quotes.delete_for_rfq("r-1", "q-1") + assert route.called + + @respx.mock + def test_accept_for_rfq_sends_put_with_body(self, comms: CommunicationsResource) -> None: + route = respx.put( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/q-1/accept", + ).mock(return_value=httpx.Response(204)) + comms.quotes.accept_for_rfq("r-1", "q-1", accepted_side="yes") + assert route.called + body = json.loads(route.calls[0].request.content) + assert body == {"accepted_side": "yes"} + + @respx.mock + def test_confirm_for_rfq_sends_put_with_empty_body(self, comms: CommunicationsResource) -> None: + route = respx.put( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/q-1/confirm", + ).mock(return_value=httpx.Response(204)) + comms.quotes.confirm_for_rfq("r-1", "q-1") + assert route.called + # json={} forces Content-Type: application/json — demo rejects empty PUTs. + assert route.calls[0].request.content == b"{}" + + @respx.mock + def test_accept_for_rfq_404(self, comms: CommunicationsResource) -> None: + respx.put( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/gone/accept", + ).mock(return_value=httpx.Response(404, json={"message": "missing"})) + with pytest.raises(KalshiNotFoundError): + comms.quotes.accept_for_rfq("r-1", "gone", accepted_side="no") + + @pytest.mark.asyncio class TestAsyncCommunications: async def test_get_id( @@ -717,6 +758,40 @@ async def test_confirm_quote( await async_comms.confirm_quote("q-1") assert route.calls[0].request.content == b"{}" + async def test_accept_for_rfq( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.put( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/q-1/accept", + ).mock(return_value=httpx.Response(204)) + await async_comms.quotes.accept_for_rfq("r-1", "q-1", accepted_side="yes") + body = json.loads(route.calls[0].request.content) + assert body == {"accepted_side": "yes"} + + async def test_confirm_for_rfq( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.put( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/q-1/confirm", + ).mock(return_value=httpx.Response(204)) + await async_comms.quotes.confirm_for_rfq("r-1", "q-1") + assert route.calls[0].request.content == b"{}" + + async def test_delete_for_rfq( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + route = respx_mock.delete( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/r-1/quotes/q-1", + ).mock(return_value=httpx.Response(204)) + await async_comms.quotes.delete_for_rfq("r-1", "q-1") + assert route.called + async def test_delete_rfq( self, async_comms: AsyncCommunicationsResource, @@ -863,6 +938,24 @@ def test_confirm_quote_requires_auth( with pytest.raises(AuthRequiredError): unauth_comms.confirm_quote("q-1") + def test_delete_for_rfq_requires_auth( + self, unauth_comms: CommunicationsResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.quotes.delete_for_rfq("r-1", "q-1") + + def test_accept_for_rfq_requires_auth( + self, unauth_comms: CommunicationsResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.quotes.accept_for_rfq("r-1", "q-1", accepted_side="yes") + + def test_confirm_for_rfq_requires_auth( + self, unauth_comms: CommunicationsResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_comms.quotes.confirm_for_rfq("r-1", "q-1") + class TestClientWiring: def test_sync_client_exposes_communications( diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 8517e36..8025774 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -102,11 +102,6 @@ def test_exclusions_bootstrap_has_cursor_entries(self) -> None: for key in cursor_keys: assert EXCLUSIONS[key].kind == "paginator_handled" - def test_exclusions_bootstrap_has_create_order_request_entries(self) -> None: - create_keys = [k for k in EXCLUSIONS if k[0] == "kalshi.models.orders.CreateOrderRequest"] - field_names = {k[1] for k in create_keys} - assert {"yes_price", "no_price", "sell_position_floor"} <= field_names - def test_method_endpoint_entry_has_request_body_schema(self) -> None: entry = MethodEndpointEntry( sdk_method="x", @@ -1195,15 +1190,9 @@ def _assert_params_match( # Registry: spec $ref → SDK request model FQN. # Update whenever a new POST/PUT/DELETE-with-body endpoint gets a request model. BODY_MODEL_MAP: dict[str, str] = { - "#/components/schemas/CreateOrderRequest": ("kalshi.models.orders.CreateOrderRequest"), - "#/components/schemas/AmendOrderRequest": ("kalshi.models.orders.AmendOrderRequest"), - "#/components/schemas/DecreaseOrderRequest": ("kalshi.models.orders.DecreaseOrderRequest"), - "#/components/schemas/BatchCreateOrdersRequest": ( - "kalshi.models.orders.BatchCreateOrdersRequest" - ), - "#/components/schemas/BatchCancelOrdersRequest": ( - "kalshi.models.orders.BatchCancelOrdersRequest" - ), + # V1 order-write request bodies (CreateOrderRequest/AmendOrderRequest/ + # DecreaseOrderRequest/BatchCreate*/BatchCancel*) were removed in spec v3.22.0 + # along with their endpoints. Only the V2 family remains. "#/components/schemas/CreateOrderV2Request": ("kalshi.models.orders.CreateOrderV2Request"), "#/components/schemas/AmendOrderV2Request": ("kalshi.models.orders.AmendOrderV2Request"), "#/components/schemas/DecreaseOrderV2Request": ("kalshi.models.orders.DecreaseOrderV2Request"), diff --git a/tests/test_extra_headers_plumbing.py b/tests/test_extra_headers_plumbing.py index caa03d5..45d9e78 100644 --- a/tests/test_extra_headers_plumbing.py +++ b/tests/test_extra_headers_plumbing.py @@ -13,6 +13,7 @@ import inspect import pkgutil +from decimal import Decimal from typing import Any import httpx @@ -22,8 +23,9 @@ from kalshi import AsyncKalshiClient, KalshiClient from kalshi.auth import KalshiAuth from kalshi.config import DEMO_WS_URL, KalshiConfig +from kalshi.models.orders import CreateOrderV2Request from kalshi.resources._base import AsyncResource, SyncResource -from tests._model_fixtures import market_dict, order_dict +from tests._model_fixtures import market_dict MOCK_BASE = "https://demo-api.kalshi.co/trade-api/v2" @@ -38,28 +40,45 @@ def _config(**overrides: Any) -> KalshiConfig: ) -def _example_order_payload() -> dict[str, Any]: - return order_dict(order_id="o1", ticker="BTC", side="yes", status="resting") - - def _example_market_payload() -> dict[str, Any]: return market_dict(ticker="BTC") +def _create_v2_request() -> CreateOrderV2Request: + return CreateOrderV2Request( + ticker="BTC", + client_order_id="cli-1", + side="bid", + count=Decimal("1"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) + + +def _create_v2_response() -> dict[str, Any]: + return { + "order_id": "o1", + "client_order_id": "cli-1", + "fill_count": "0", + "remaining_count": "1", + "ts_ms": 1700000000000, + } + + +def _cancel_v2_response() -> dict[str, Any]: + return {"order_id": "o1", "reduced_by": "0", "ts_ms": 1700000000000} + + class TestOrders: @respx.mock def test_create_order_threads_extra_headers(self, test_auth: KalshiAuth) -> None: - route = respx.post(f"{MOCK_BASE}/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _example_order_payload()}) + route = respx.post(f"{MOCK_BASE}/portfolio/events/orders").mock( + return_value=httpx.Response(201, json=_create_v2_response()) ) with KalshiClient(auth=test_auth, config=_config()) as client: - client.orders.create( - ticker="BTC", - side="yes", - action="buy", - client_order_id="cli-1", - count=1, - yes_price=50, + client.orders.create_v2( + request=_create_v2_request(), extra_headers={"Idempotency-Key": "abc"}, ) assert route.calls[0].request.headers["Idempotency-Key"] == "abc" @@ -68,20 +87,15 @@ def test_create_order_threads_extra_headers(self, test_auth: KalshiAuth) -> None def test_create_order_extra_headers_rejects_kalshi_access(self, test_auth: KalshiAuth) -> None: # #298: caller-supplied KALSHI-ACCESS-* keys now raise instead of # silently leaking onto the wire alongside the SDK-signed pair. - respx.post(f"{MOCK_BASE}/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _example_order_payload()}) + respx.post(f"{MOCK_BASE}/portfolio/events/orders").mock( + return_value=httpx.Response(201, json=_create_v2_response()) ) with ( KalshiClient(auth=test_auth, config=_config()) as client, pytest.raises(ValueError, match="KALSHI-ACCESS-"), ): - client.orders.create( - ticker="BTC", - side="yes", - action="buy", - client_order_id="cli-1", - count=1, - yes_price=50, + client.orders.create_v2( + request=_create_v2_request(), extra_headers={ "KALSHI-ACCESS-KEY": "spoofed", "KALSHI-ACCESS-SIGNATURE": "spoofed-sig", @@ -91,11 +105,11 @@ def test_create_order_extra_headers_rejects_kalshi_access(self, test_auth: Kalsh @respx.mock def test_cancel_order_threads_extra_headers(self, test_auth: KalshiAuth) -> None: - route = respx.delete(f"{MOCK_BASE}/portfolio/orders/o1").mock( - return_value=httpx.Response(204) + route = respx.delete(f"{MOCK_BASE}/portfolio/events/orders/o1").mock( + return_value=httpx.Response(200, json=_cancel_v2_response()) ) with KalshiClient(auth=test_auth, config=_config()) as client: - client.orders.cancel("o1", extra_headers={"X-Trace": "t"}) + client.orders.cancel_v2("o1", extra_headers={"X-Trace": "t"}) assert route.calls[0].request.headers["X-Trace"] == "t" @@ -173,20 +187,15 @@ def test_extra_headers_lowercase_kalshi_access_raises( ) -> None: # Case-mismatched key (lowercase) was previously a forge surface; the # public boundary now raises ValueError naming the leaked key. - respx.post(f"{MOCK_BASE}/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _example_order_payload()}) + respx.post(f"{MOCK_BASE}/portfolio/events/orders").mock( + return_value=httpx.Response(201, json=_create_v2_response()) ) with ( KalshiClient(auth=test_auth, config=_config()) as client, pytest.raises(ValueError, match="kalshi-access-key"), ): - client.orders.create( - ticker="BTC", - side="yes", - action="buy", - client_order_id="cli-1", - count=1, - yes_price=50, + client.orders.create_v2( + request=_create_v2_request(), extra_headers={"kalshi-access-key": "X"}, ) @@ -197,17 +206,12 @@ def test_post_json_body_pins_content_type_against_caller_override( # Caller-supplied lowercase ``content-type`` must NOT win over the # JSON body's content-type, which the SDK pins at the body-helper # layer in resources/_base.py:_post. - route = respx.post(f"{MOCK_BASE}/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _example_order_payload()}) + route = respx.post(f"{MOCK_BASE}/portfolio/events/orders").mock( + return_value=httpx.Response(201, json=_create_v2_response()) ) with KalshiClient(auth=test_auth, config=_config()) as client: - client.orders.create( - ticker="BTC", - side="yes", - action="buy", - client_order_id="cli-1", - count=1, - yes_price=50, + client.orders.create_v2( + request=_create_v2_request(), extra_headers={"content-type": "text/plain"}, ) wire = route.calls.last.request.headers diff --git a/tests/test_model_extra_policy.py b/tests/test_model_extra_policy.py index 69d8a5c..e519ebd 100644 --- a/tests/test_model_extra_policy.py +++ b/tests/test_model_extra_policy.py @@ -15,7 +15,7 @@ # Request bodies must stay `extra="forbid"`. Identified by name suffix per # the CLAUDE.md "Adding a new resource" convention. Both `Request` and the -# longer `RequestOrder` are needed: `BatchCancelOrdersRequestOrder` is a +# longer `RequestOrder` are needed: `BatchCancelOrdersV2RequestOrder` is a # request-body sub-model whose name doesn't end in plain `Request`. _REQUEST_BODY_SUFFIXES = ("Request", "RequestOrder") diff --git a/tests/test_models.py b/tests/test_models.py index 4c44f6b..9da9686 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -215,21 +215,6 @@ def test_candlestick_nested_structure(self) -> None: assert c.price.close == Decimal("0.5500") assert c.volume == Decimal("100.00") - def test_create_order_serializes_with_dollars_alias(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="T", - side="yes", - action="buy", - count=1, - yes_price=Decimal("0.65"), - ) - data = req.model_dump(mode="json", exclude_none=True, by_alias=True) - assert "yes_price_dollars" in data - assert data["yes_price_dollars"] == "0.65" - assert "yes_price" not in data - class TestMarketV3180Fields: """v3.18.0 backfill (issue #159): 11 new optional fields on ``Market``. @@ -744,31 +729,6 @@ def test_validation_has_details(self) -> None: assert err.details == {"field": "required"} -class TestAmendOrderResponse: - def test_parses_old_and_new_order(self) -> None: - from kalshi.models.orders import AmendOrderResponse - - data = { - "old_order": order_dict( - order_id="ord-old", - ticker="MKT-A", - yes_price_dollars="0.5000", - initial_count_fp=5, - ), - "order": order_dict( - order_id="ord-new", - ticker="MKT-A", - yes_price_dollars="0.6500", - initial_count_fp=5, - ), - } - result = AmendOrderResponse.model_validate(data) - assert result.old_order.order_id == "ord-old" - assert result.order.order_id == "ord-new" - assert result.old_order.yes_price == Decimal("0.5000") - assert result.order.yes_price == Decimal("0.6500") - - class TestOrderQueuePosition: def test_parses_queue_position(self) -> None: from kalshi.models.orders import OrderQueuePosition @@ -784,475 +744,6 @@ def test_parses_queue_position(self) -> None: assert result.queue_position == Decimal("42.00") -class TestCreateOrderRequestExtended: - def test_accepts_time_in_force(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - time_in_force="fill_or_kill", - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["time_in_force"] == "fill_or_kill" - - def test_accepts_post_only_and_reduce_only(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - post_only=True, - reduce_only=False, - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["post_only"] is True - assert body["reduce_only"] is False - - def test_accepts_self_trade_prevention_and_order_group(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - self_trade_prevention_type="maker", - order_group_id="grp-123", - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["self_trade_prevention_type"] == "maker" - assert body["order_group_id"] == "grp-123" - - def test_accepts_cancel_on_pause_and_subaccount(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - cancel_order_on_pause=True, - subaccount=5, - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["cancel_order_on_pause"] is True - assert body["subaccount"] == 5 - - def test_buy_max_cost_is_int_cents(self) -> None: - """Spec says integer cents; SDK must send int on the wire.""" - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=500, - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["buy_max_cost"] == 500 - assert isinstance(body["buy_max_cost"], int) - - def test_buy_max_cost_rejects_fractional_value(self) -> None: - """A caller passing a fractional string like '5.5' must raise. - - Pydantic v2 int coercion rejects strings that are not whole numbers - (e.g. '5.5'), but accepts whole-number strings like '500' and even - '5.00' (coerced to 5). The field is int cents, so fractional values - are always invalid. - """ - from pydantic import ValidationError - - from kalshi.models.orders import CreateOrderRequest - - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost="5.5", # type: ignore[arg-type] - ) - - def test_buy_max_cost_rejects_decimal(self) -> None: - """Migration-hazard guard: Decimal inputs raise clearly, not silently coerce.""" - from decimal import Decimal - - from pydantic import ValidationError - - from kalshi.models.orders import CreateOrderRequest - - # Both whole and fractional Decimal values must raise — the hazard is - # silent coercion to cents regardless of the numeric value. - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=Decimal("500"), # type: ignore[arg-type] - ) - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=Decimal("5.00"), # type: ignore[arg-type] - ) - - def test_buy_max_cost_rejects_float(self) -> None: - """Float inputs (even whole-valued) must raise to prevent unit confusion.""" - from pydantic import ValidationError - - from kalshi.models.orders import CreateOrderRequest - - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=5.0, # type: ignore[arg-type] - ) - - def test_buy_max_cost_rejects_bool(self) -> None: - """#243: bool inputs must raise — bool is an ``int`` subclass, so - ``True`` would otherwise slip through as ``1`` (= 1 cent cap), a - silent money-risk failure matching the #225 class of bug for - ``DollarDecimal`` / ``FixedPointCount``. A caller who passes a flag - (``risk_check_passed``, ``dry_run``) where cents were expected must - get a clear error, not a $0.01-capped order.""" - from pydantic import ValidationError - - from kalshi.models.orders import CreateOrderRequest - - with pytest.raises(ValidationError, match=r"bool"): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=True, # type: ignore[arg-type] - ) - with pytest.raises(ValidationError, match=r"bool"): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=False, # type: ignore[arg-type] - ) - - def test_buy_max_cost_accepts_plain_int(self) -> None: - """#243: regression — plain int (cents) still works.""" - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost=500, - ) - assert req.buy_max_cost == 500 - assert isinstance(req.buy_max_cost, int) - assert not isinstance(req.buy_max_cost, bool) - - def test_buy_max_cost_accepts_int_string(self) -> None: - """Int-shaped strings are coerced normally (e.g., loading from env/config).""" - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - buy_max_cost="500", # type: ignore[arg-type] - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["buy_max_cost"] == 500 - - def test_omits_none_fields_from_wire(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest(ticker="MKT", side="yes", action="buy", count=1) - body = req.model_dump(exclude_none=True, by_alias=True) - # Core fields present - assert body["ticker"] == "MKT" - # Optional fields absent (defaults to None, stripped by exclude_none) - assert "time_in_force" not in body - assert "post_only" not in body - assert "buy_max_cost" not in body - assert "subaccount" not in body - - def test_phantom_type_field_removed(self) -> None: - """v0.8.0 removed the phantom `type` field (spec has no such field).""" - from pydantic import ValidationError - - from kalshi.models.orders import CreateOrderRequest - - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - type="limit", # type: ignore[call-arg] - ) - - def test_forbid_extra_rejects_unknown_kwarg(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import CreateOrderRequest - - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=1, - bogus_field="x", # type: ignore[call-arg] - ) - - def test_serializes_count_fp_not_count(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - req = CreateOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=Decimal("7"), - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert "count_fp" in body - assert "count" not in body - - -class TestAmendOrderRequest: - def test_required_fields(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import AmendOrderRequest - - with pytest.raises(ValidationError): - AmendOrderRequest() # type: ignore[call-arg] - - # ticker/side/action required per spec - req = AmendOrderRequest(ticker="MKT", side="yes", action="buy") - assert req.ticker == "MKT" - - def test_serializes_yes_price_dollars(self) -> None: - from decimal import Decimal - - from kalshi.models.orders import AmendOrderRequest - - req = AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - yes_price=Decimal("0.55"), - ) - body = req.model_dump(exclude_none=True, by_alias=True, mode="json") - assert body["yes_price_dollars"] == "0.55" - assert "yes_price" not in body # int cent form excluded - - def test_serializes_no_price_dollars(self) -> None: - from decimal import Decimal - - from kalshi.models.orders import AmendOrderRequest - - req = AmendOrderRequest( - ticker="MKT", - side="no", - action="sell", - no_price=Decimal("0.75"), - ) - body = req.model_dump(exclude_none=True, by_alias=True, mode="json") - assert body["no_price_dollars"] == "0.75" - assert "no_price" not in body - - def test_serializes_count_fp(self) -> None: - from decimal import Decimal - - from kalshi.models.orders import AmendOrderRequest - - req = AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - count=Decimal("3"), - ) - body = req.model_dump(exclude_none=True, by_alias=True, mode="json") - assert "count_fp" in body - assert body["count_fp"] == "3" - assert "count" not in body - - def test_forbid_extra(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import AmendOrderRequest - - with pytest.raises(ValidationError): - AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - bogus_field="x", # type: ignore[call-arg] - ) - - def test_accepts_client_order_ids(self) -> None: - from kalshi.models.orders import AmendOrderRequest - - req = AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - client_order_id="old-id", - updated_client_order_id="new-id", - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["client_order_id"] == "old-id" - assert body["updated_client_order_id"] == "new-id" - - def test_accepts_subaccount(self) -> None: - from kalshi.models.orders import AmendOrderRequest - - req = AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - subaccount=3, - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["subaccount"] == 3 - - -class TestDecreaseOrderRequest: - def test_accepts_reduce_by(self) -> None: - from kalshi.models.orders import DecreaseOrderRequest - - req = DecreaseOrderRequest(reduce_by=3) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body == {"reduce_by": 3} - - def test_accepts_reduce_to(self) -> None: - from kalshi.models.orders import DecreaseOrderRequest - - req = DecreaseOrderRequest(reduce_to=2) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body == {"reduce_to": 2} - - def test_accepts_subaccount(self) -> None: - from kalshi.models.orders import DecreaseOrderRequest - - req = DecreaseOrderRequest(reduce_by=1, subaccount=4) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body["subaccount"] == 4 - - def test_forbid_extra(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import DecreaseOrderRequest - - with pytest.raises(ValidationError): - DecreaseOrderRequest( - reduce_by=1, - bogus_field=5, # type: ignore[call-arg] - ) - - def test_rejects_both_reduce_by_and_reduce_to(self) -> None: - """Model rejects setting both fields — direct construction must match - the method-level guard in ``orders.decrease()``. - """ - from pydantic import ValidationError - - from kalshi.models.orders import DecreaseOrderRequest - - with pytest.raises(ValidationError, match="not both"): - DecreaseOrderRequest(reduce_by=3, reduce_to=2) - - def test_rejects_neither_reduce_by_nor_reduce_to(self) -> None: - """Model rejects the all-None case too: sending an empty decrease body - is meaningless, so fail-fast at construction matches the method-level - guard and keeps the v0.9 model-first API honest. - """ - from pydantic import ValidationError - - from kalshi.models.orders import DecreaseOrderRequest - - with pytest.raises(ValidationError, match="reduce_by or reduce_to"): - DecreaseOrderRequest() - - -class TestBatchCreateOrdersRequest: - def test_wraps_order_list(self) -> None: - from kalshi.models.orders import ( - BatchCreateOrdersRequest, - CreateOrderRequest, - ) - - orders = [ - CreateOrderRequest(ticker="MKT-A", side="yes", action="buy", count=1), - CreateOrderRequest(ticker="MKT-B", side="no", action="sell", count=1), - ] - req = BatchCreateOrdersRequest(orders=orders) - body = req.model_dump(exclude_none=True, by_alias=True) - - assert "orders" in body - assert len(body["orders"]) == 2 - assert body["orders"][0]["ticker"] == "MKT-A" - assert body["orders"][1]["ticker"] == "MKT-B" - - def test_empty_list_allowed(self) -> None: - from kalshi.models.orders import BatchCreateOrdersRequest - - req = BatchCreateOrdersRequest(orders=[]) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body == {"orders": []} - - def test_forbid_extra(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import BatchCreateOrdersRequest - - with pytest.raises(ValidationError): - BatchCreateOrdersRequest( - orders=[], - bogus=1, # type: ignore[call-arg] - ) - - def test_nested_create_order_phantom_rejected(self) -> None: - """Phantom key in a raw-dict nested order rejected via BatchCreateOrdersRequest. - - Constructs BatchCreateOrdersRequest with a raw dict item — Pydantic - coerces into CreateOrderRequest and its extra='forbid' fires on the - phantom. This exercises the BatchCreateOrdersRequest -> nested item - path, not CreateOrderRequest's forbid in isolation (already covered - by TestCreateOrderRequestExtended.test_forbid_extra). - """ - from pydantic import ValidationError - - from kalshi.models.orders import BatchCreateOrdersRequest - - with pytest.raises(ValidationError): - BatchCreateOrdersRequest( - orders=[ - { - "ticker": "MKT", - "side": "yes", - "action": "buy", - "type": "limit", # phantom - }, # type: ignore[list-item] - ], - ) - - class TestCreateMarketInMultivariateRequest: def test_requires_selected_markets(self) -> None: from pydantic import ValidationError @@ -1334,79 +825,6 @@ def test_forbid_extra(self) -> None: ) -class TestBatchCancelOrdersRequest: - def test_orders_field_required(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import BatchCancelOrdersRequest - - with pytest.raises(ValidationError): - BatchCancelOrdersRequest() # type: ignore[call-arg] - - def test_empty_orders_list_allowed(self) -> None: - from kalshi.models.orders import BatchCancelOrdersRequest - - req = BatchCancelOrdersRequest(orders=[]) - body = req.model_dump(exclude_none=True, by_alias=True) - assert body == {"orders": []} - - def test_wraps_order_entries(self) -> None: - from kalshi.models.orders import ( - BatchCancelOrdersRequest, - BatchCancelOrdersRequestOrder, - ) - - req = BatchCancelOrdersRequest( - orders=[ - BatchCancelOrdersRequestOrder(order_id="ord-1"), - BatchCancelOrdersRequestOrder(order_id="ord-2", subaccount=3), - ], - ) - body = req.model_dump(exclude_none=True, by_alias=True) - assert len(body["orders"]) == 2 - assert body["orders"][0] == {"order_id": "ord-1"} - assert body["orders"][1] == {"order_id": "ord-2", "subaccount": 3} - - def test_forbid_extra_on_wrapper(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import BatchCancelOrdersRequest - - with pytest.raises(ValidationError): - BatchCancelOrdersRequest( - orders=[], - bogus=1, # type: ignore[call-arg] - ) - - -class TestBatchCancelOrdersRequestOrder: - def test_order_id_required(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import BatchCancelOrdersRequestOrder - - with pytest.raises(ValidationError): - BatchCancelOrdersRequestOrder() # type: ignore[call-arg] - - def test_subaccount_optional(self) -> None: - from kalshi.models.orders import BatchCancelOrdersRequestOrder - - req = BatchCancelOrdersRequestOrder(order_id="ord-x") - body = req.model_dump(exclude_none=True, by_alias=True) - assert body == {"order_id": "ord-x"} - - def test_forbid_extra(self) -> None: - from pydantic import ValidationError - - from kalshi.models.orders import BatchCancelOrdersRequestOrder - - with pytest.raises(ValidationError): - BatchCancelOrdersRequestOrder( - order_id="x", - bogus=5, # type: ignore[call-arg] - ) - - # ---------- P2#4: AwareDatetime on REST response models ---------- @@ -1526,59 +944,6 @@ class TestStrictIntRejectsBoolOnRequestModels: @pytest.mark.parametrize( ("model_path", "field_name", "other_kwargs"), [ - # CreateOrderRequest - ( - "kalshi.models.orders:CreateOrderRequest", - "subaccount", - {"ticker": "MKT", "side": "yes", "action": "buy", "count": 1}, - ), - ( - "kalshi.models.orders:CreateOrderRequest", - "exchange_index", - {"ticker": "MKT", "side": "yes", "action": "buy", "count": 1}, - ), - ( - "kalshi.models.orders:CreateOrderRequest", - "expiration_ts", - {"ticker": "MKT", "side": "yes", "action": "buy", "count": 1}, - ), - # AmendOrderRequest - ( - "kalshi.models.orders:AmendOrderRequest", - "subaccount", - {"ticker": "MKT", "side": "yes", "action": "buy"}, - ), - ( - "kalshi.models.orders:AmendOrderRequest", - "exchange_index", - {"ticker": "MKT", "side": "yes", "action": "buy"}, - ), - # DecreaseOrderRequest — reduce_by/reduce_to are XOR with each other, - # so each row pairs the StrictInt-under-test against a valid sibling - # where required. subaccount/exchange_index need a real reduce_* set. - ("kalshi.models.orders:DecreaseOrderRequest", "reduce_by", {}), - ("kalshi.models.orders:DecreaseOrderRequest", "reduce_to", {}), - ( - "kalshi.models.orders:DecreaseOrderRequest", - "subaccount", - {"reduce_by": 1}, - ), - ( - "kalshi.models.orders:DecreaseOrderRequest", - "exchange_index", - {"reduce_by": 1}, - ), - # BatchCancelOrdersRequestOrder - ( - "kalshi.models.orders:BatchCancelOrdersRequestOrder", - "subaccount", - {"order_id": "abc"}, - ), - ( - "kalshi.models.orders:BatchCancelOrdersRequestOrder", - "exchange_index", - {"order_id": "abc"}, - ), # Order groups ( "kalshi.models.order_groups:CreateOrderGroupRequest", @@ -1735,44 +1100,6 @@ def test_strict_int_rejects_bool_on_request_models( model_cls(**{**other_kwargs, field_name: bool_value}) -class TestIssue312AmendOrderRequestLiteralNarrowing: - """#312: AmendOrderRequest.side/.action narrowed to Literal aliases. - - Mirrors the v2.5 #270 narrowing on CreateOrderRequest. A typo like - ``side="yess"`` now fails at construction rather than at the server's - 400 response after a signed HTTP round-trip. - """ - - def test_issue_312_amend_order_request_rejects_invalid_side(self) -> None: - from kalshi.models.orders import AmendOrderRequest - - with pytest.raises(ValidationError): - AmendOrderRequest( - ticker="MKT", - side="yess", # type: ignore[arg-type] - action="buy", - count=Decimal(1), - ) - - def test_issue_312_amend_order_request_rejects_invalid_action(self) -> None: - from kalshi.models.orders import AmendOrderRequest - - with pytest.raises(ValidationError): - AmendOrderRequest( - ticker="MKT", - side="yes", - action="buyy", # type: ignore[arg-type] - count=Decimal(1), - ) - - def test_issue_312_amend_order_request_accepts_valid_literals(self) -> None: - from kalshi.models.orders import AmendOrderRequest - - req = AmendOrderRequest(ticker="MKT", side="no", action="sell") - assert req.side == "no" - assert req.action == "sell" - - class TestIssue326V1SubaccountGeZero: """#326: V1 order request models gain ``ge=0`` on subaccount-shaped ints. @@ -1787,50 +1114,6 @@ class TestIssue326V1SubaccountGeZero: @pytest.mark.parametrize( ("model_path", "field_name", "other_kwargs"), [ - # V1 CreateOrderRequest - ( - "kalshi.models.orders:CreateOrderRequest", - "subaccount", - {"ticker": "MKT", "side": "yes", "action": "buy", "count": 1}, - ), - ( - "kalshi.models.orders:CreateOrderRequest", - "exchange_index", - {"ticker": "MKT", "side": "yes", "action": "buy", "count": 1}, - ), - # V1 AmendOrderRequest - ( - "kalshi.models.orders:AmendOrderRequest", - "subaccount", - {"ticker": "MKT", "side": "yes", "action": "buy"}, - ), - ( - "kalshi.models.orders:AmendOrderRequest", - "exchange_index", - {"ticker": "MKT", "side": "yes", "action": "buy"}, - ), - # V1 DecreaseOrderRequest - ( - "kalshi.models.orders:DecreaseOrderRequest", - "subaccount", - {"reduce_by": 1}, - ), - ( - "kalshi.models.orders:DecreaseOrderRequest", - "exchange_index", - {"reduce_by": 1}, - ), - # V1 BatchCancelOrdersRequestOrder - ( - "kalshi.models.orders:BatchCancelOrdersRequestOrder", - "subaccount", - {"order_id": "abc"}, - ), - ( - "kalshi.models.orders:BatchCancelOrdersRequestOrder", - "exchange_index", - {"order_id": "abc"}, - ), # V2 exchange_index slots that were missed in #295 ( "kalshi.models.orders:CreateOrderV2Request", @@ -1845,9 +1128,12 @@ class TestIssue326V1SubaccountGeZero: "self_trade_prevention_type": "maker", }, ), + # BatchCancelOrdersV2RequestOrder.exchange_index intentionally has NO + # ge=0 floor (spec v3.22.0 allows -1 to auto-route by market_ticker), + # so the ge=0 assertion targets its ``subaccount`` field instead. ( "kalshi.models.orders:BatchCancelOrdersV2RequestOrder", - "exchange_index", + "subaccount", {"order_id": "abc"}, ), ], @@ -1937,49 +1223,16 @@ class TestIssue343DollarDecimalRequestBounds: response-side PnL/fee/settlement fields where negatives and arbitrary precision are legitimate. Request-side price fields, however, are bounded by Kalshi's order contract: non-negative and aligned to the $0.0001 tick. - The :data:`OrderPrice` alias applied on CreateOrderRequest / AmendOrderRequest - / CreateOrderV2Request / AmendOrderV2Request enforces both at construction. + The :data:`OrderPrice` alias applied on CreateOrderV2Request / + AmendOrderV2Request enforces both at construction. """ def test_issue_343_dollar_decimal_request_rejects_negative_and_invalid_tick(self) -> None: from kalshi.models.orders import ( - AmendOrderRequest, AmendOrderV2Request, - CreateOrderRequest, CreateOrderV2Request, ) - # V1 CreateOrderRequest: yes_price / no_price negative -> ValidationError - with pytest.raises(ValidationError, match="non-negative"): - CreateOrderRequest( - ticker="T", side="yes", action="buy", - count=Decimal("1"), yes_price=Decimal("-0.65"), - ) - with pytest.raises(ValidationError, match="non-negative"): - CreateOrderRequest( - ticker="T", side="no", action="buy", - count=Decimal("1"), no_price=Decimal("-0.01"), - ) - - # Sub-tick precision -> ValidationError - with pytest.raises(ValidationError, match=r"\$0\.0001 tick"): - CreateOrderRequest( - ticker="T", side="yes", action="buy", - count=Decimal("1"), yes_price=Decimal("0.12345"), - ) - - # V1 AmendOrderRequest mirrors V1 Create. - with pytest.raises(ValidationError, match="non-negative"): - AmendOrderRequest( - ticker="T", side="yes", action="buy", - yes_price=Decimal("-0.5"), - ) - with pytest.raises(ValidationError, match=r"\$0\.0001 tick"): - AmendOrderRequest( - ticker="T", side="no", action="sell", - no_price=Decimal("0.99999"), - ) - # V2 CreateOrderV2Request. with pytest.raises(ValidationError, match="non-negative"): CreateOrderV2Request( @@ -2008,22 +1261,6 @@ def test_issue_343_dollar_decimal_request_rejects_negative_and_invalid_tick(self price=Decimal("0.12345"), count=Decimal("1"), ) - def test_issue_343_accepts_zero_and_tick_aligned_prices(self) -> None: - from kalshi.models.orders import CreateOrderRequest - - # Zero is non-negative; round-tick four-decimal value is aligned. - req = CreateOrderRequest( - ticker="T", side="yes", action="buy", - count=Decimal("1"), yes_price=Decimal("0"), - ) - assert req.yes_price == Decimal("0") - - req = CreateOrderRequest( - ticker="T", side="yes", action="buy", - count=Decimal("1"), yes_price=Decimal("0.5600"), - ) - assert req.yes_price == Decimal("0.5600") - def test_issue_343_response_dollar_decimal_unchanged(self) -> None: # Response-side averages (V2) keep bare DollarDecimal — negatives must # remain legal for PnL/fee/settlement parity. Pin the contract. diff --git a/tests/test_orders.py b/tests/test_orders.py index 6b3dd0a..29fa8d3 100644 --- a/tests/test_orders.py +++ b/tests/test_orders.py @@ -9,24 +9,20 @@ import httpx import pytest import respx -from pydantic import ValidationError from kalshi._base_client import SyncTransport from kalshi.auth import KalshiAuth -from kalshi.client import KalshiClient from kalshi.config import KalshiConfig from kalshi.errors import ( AuthRequiredError, KalshiError, KalshiNotFoundError, - KalshiValidationError, ) from kalshi.models.orders import ( AmendOrderV2Request, BatchCancelOrdersV2Request, BatchCancelOrdersV2RequestOrder, BatchCreateOrdersV2Request, - CreateOrderRequest, CreateOrderV2Request, DecreaseOrderV2Request, ) @@ -48,298 +44,11 @@ def orders(test_auth: KalshiAuth, config: KalshiConfig) -> OrdersResource: return OrdersResource(SyncTransport(test_auth, config)) -@pytest.fixture -def client(test_auth: KalshiAuth) -> KalshiClient: - """KalshiClient wired to the demo base URL (matches wire-shape test mocks).""" - from kalshi.config import DEMO_BASE_URL, DEMO_WS_URL - - cfg = KalshiConfig(base_url=DEMO_BASE_URL, ws_base_url=DEMO_WS_URL, timeout=5.0, max_retries=0) - return KalshiClient(auth=test_auth, config=cfg) - - @pytest.fixture def unauth_orders(config: KalshiConfig) -> OrdersResource: return OrdersResource(SyncTransport(None, config)) -_MINIMAL_ORDER = order_dict(order_id="ord-123", ticker="MKT", side="yes", status="resting") - - -class TestCreateOrderWireShape: - """v0.8.0: orders.create() builds CreateOrderRequest internally and - serializes via model_dump. Wire body must not contain phantom `type` - field; count must serialize as count_fp; new fields reach the wire.""" - - def test_no_phantom_type_in_wire( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - import json - - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create(ticker="MKT", side="yes", action="buy", count=1, yes_price=0.5) - - body = json.loads(route.calls[0].request.content) - assert "type" not in body - - def test_count_fp_not_count_in_wire( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - import json - - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create(ticker="MKT", side="yes", action="buy", yes_price=0.5, count=3) - - body = json.loads(route.calls[0].request.content) - assert "count_fp" in body - assert body["count_fp"] == "3" - assert "count" not in body - - def test_time_in_force_reaches_wire( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - import json - - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - time_in_force="fill_or_kill", - ) - - body = json.loads(route.calls[0].request.content) - assert body["time_in_force"] == "fill_or_kill" - - def test_post_only_reduce_only_reach_wire( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - import json - - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - post_only=True, - reduce_only=False, - ) - - body = json.loads(route.calls[0].request.content) - assert body["post_only"] is True - assert body["reduce_only"] is False - - def test_buy_max_cost_int_cents_wire( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """Spec says cents. SDK must send int on the wire.""" - import json - - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create( - ticker="MKT", side="yes", action="buy", count=1, yes_price=0.5, buy_max_cost=500 - ) - - body = json.loads(route.calls[0].request.content) - assert body["buy_max_cost"] == 500 - assert isinstance(body["buy_max_cost"], int) - - def test_subaccount_order_group_cancel_on_pause_stp_wire( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - import json - - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create( - ticker="MKT", - side="yes", - action="buy", - count=1, - yes_price=0.5, - subaccount=2, - order_group_id="grp-x", - cancel_order_on_pause=True, - self_trade_prevention_type="maker", - ) - - body = json.loads(route.calls[0].request.content) - assert body["subaccount"] == 2 - assert body["order_group_id"] == "grp-x" - assert body["cancel_order_on_pause"] is True - assert body["self_trade_prevention_type"] == "maker" - - def test_type_kwarg_removed(self, client: KalshiClient) -> None: - """v0.8.0 removed the `type` kwarg from orders.create().""" - with pytest.raises(TypeError): - client.orders.create( - ticker="MKT", - side="yes", - type="market", # type: ignore[call-arg] - ) - - def test_missing_count_and_action_raises_before_http( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """#242: kwarg-form create() with missing ``count`` / ``action`` must raise - ``TypeError`` BEFORE any HTTP request is dispatched. Pre-#242 the SDK - silently defaulted to count=1, action="buy" — converting a missing-arg - bug into a real 1-contract BUY fill (money risk).""" - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - with pytest.raises(TypeError, match=r"count.*action"): - client.orders.create(ticker="X", side="yes") - with pytest.raises(TypeError, match=r"count.*action"): - client.orders.create(ticker="X", side="yes", action="buy") - with pytest.raises(TypeError, match=r"count.*action"): - client.orders.create(ticker="X", side="yes", count=10) - - assert route.call_count == 0 - - def test_explicit_count_action_kwargs_still_work( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """#242: regression — explicit ``count`` + ``action`` kwargs still build - and dispatch normally.""" - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create(ticker="X", side="yes", count=10, action="buy", yes_price="0.5") - - assert route.call_count == 1 - body = json.loads(route.calls[0].request.content) - assert body["count_fp"] == "10" - assert body["action"] == "buy" - assert body["yes_price_dollars"] == "0.5" - - def test_request_overload_unaffected_by_242( - self, - client: KalshiClient, - respx_mock: respx.MockRouter, - ) -> None: - """#242: the ``request=CreateOrderRequest(...)`` path is unaffected by - the kwarg-overload guard — the model itself now declares count/action - required, so a fully-populated request still dispatches.""" - route = respx_mock.post("https://demo-api.kalshi.co/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json={"order": _MINIMAL_ORDER}) - ) - - client.orders.create( - request=CreateOrderRequest( - ticker="X", - side="yes", - count=Decimal("10"), - action="buy", - yes_price=Decimal("0.5"), - ) - ) - - assert route.call_count == 1 - body = json.loads(route.calls[0].request.content) - assert body["count_fp"] == "10" - assert body["action"] == "buy" - - -class TestOrdersCreate: - @respx.mock - def test_create_limit_order(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-123", - ticker="TEST-MKT", - side="yes", - status="resting", - yes_price_dollars="0.6500", - count_fp="10", - ) - }, - ) - ) - order = orders.create(ticker="TEST-MKT", side="yes", action="buy", count=10, yes_price=0.65) - assert order.order_id == "ord-123" - assert order.yes_price == Decimal("0.6500") - assert order.count == 10 - - @respx.mock - def test_create_order_no_price(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict(order_id="ord-456", ticker="TEST-MKT", status="executed") - }, - ) - ) - order = orders.create(ticker="TEST-MKT", side="yes", action="buy", count=1) - assert order.order_id == "ord-456" - - @respx.mock - def test_decimal_price_conversion(self, orders: OrdersResource) -> None: - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response( - 200, json={"order": order_dict(order_id="ord-789", ticker="T")} - ) - ) - orders.create(ticker="T", side="yes", action="buy", count=1, yes_price=0.65) - - import json - - body = json.loads(route.calls[0].request.content) - # Must be sent as yes_price_dollars (FixedPointDollars string) - assert body["yes_price_dollars"] == "0.65" - assert "yes_price" not in body - - @respx.mock - def test_validation_error(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(400, json={"message": "invalid ticker"}) - ) - with pytest.raises(KalshiValidationError): - orders.create(ticker="INVALID", side="yes", action="buy", count=1) - - class TestOrdersGet: @respx.mock def test_returns_order(self, orders: OrdersResource) -> None: @@ -362,24 +71,6 @@ def test_not_found(self, orders: OrdersResource) -> None: orders.get("fake") -class TestOrdersCancel: - @respx.mock - def test_cancel_order(self, orders: OrdersResource) -> None: - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-123").mock( - return_value=httpx.Response(200, json={}) - ) - orders.cancel("ord-123") # should not raise - - @respx.mock - def test_cancel_with_subaccount(self, orders: OrdersResource) -> None: - route = respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-456").mock( - return_value=httpx.Response(200, json={}) - ) - orders.cancel("ord-456", subaccount=42) - params = dict(route.calls[0].request.url.params) - assert params["subaccount"] == "42" - - class TestOrdersList: @respx.mock def test_returns_page(self, orders: OrdersResource) -> None: @@ -509,79 +200,6 @@ def test_list_all_with_all_new_filters(self, orders: OrdersResource) -> None: assert params["subaccount"] == "7" -class TestOrdersBatch: - @respx.mock - def test_batch_create(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order": order_dict(order_id="b1", ticker="A"), - "error": None, - "client_order_id": "c1", - }, - { - "order": order_dict(order_id="b2", ticker="B"), - "error": None, - "client_order_id": "c2", - }, - ] - }, - ) - ) - reqs = [ - CreateOrderRequest(ticker="A", side="yes", action="buy", count=1), - CreateOrderRequest(ticker="B", side="no", action="buy", count=1), - ] - from kalshi.models.orders import BatchCreateOrdersResponse - - result = orders.batch_create(reqs) - assert isinstance(result, BatchCreateOrdersResponse) - assert len(result.orders) == 2 - assert result.orders[0].order is not None - assert result.orders[0].order.order_id == "b1" - assert result.orders[0].client_order_id == "c1" - - @respx.mock - def test_batch_create_partial_failure_does_not_crash(self, orders: OrdersResource) -> None: - """#194: a failed leg returns ``{"order": null, "error": {...}}``. - - The old SDK called ``Order.model_validate(None)`` and tore down the - whole result; the typed envelope must preserve the per-leg shape. - """ - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order": order_dict(order_id="ok", ticker="A"), - "error": None, - "client_order_id": "good", - }, - { - "order": None, - "error": {"code": "bad_price", "message": "rejected"}, - "client_order_id": "bad", - }, - ] - }, - ) - ) - result = orders.batch_create( - [CreateOrderRequest(ticker="A", side="yes", action="buy", count=1)] - ) - assert len(result.orders) == 2 - assert result.orders[0].order is not None - assert result.orders[0].error is None - assert result.orders[1].order is None - assert result.orders[1].error is not None - assert result.orders[1].error["code"] == "bad_price" - assert result.orders[1].client_order_id == "bad" - - class TestOrdersFills: @respx.mock def test_returns_fills(self, orders: OrdersResource) -> None: @@ -679,155 +297,6 @@ def test_fills_all_with_all_new_filters(self, orders: OrdersResource) -> None: assert params["subaccount"] == "7" -class TestOrdersAmend: - @respx.mock - def test_amend_price(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-100/amend").mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict( - order_id="ord-100", - ticker="MKT-A", - side="yes", - status="resting", - yes_price_dollars="0.5000", - count_fp="10", - ), - "order": order_dict( - order_id="ord-100", - ticker="MKT-A", - side="yes", - status="resting", - yes_price_dollars="0.6000", - count_fp="10", - ), - }, - ) - ) - result = orders.amend( - "ord-100", - ticker="MKT-A", - side="yes", - action="buy", - yes_price=0.60, - ) - assert result.old_order.yes_price == Decimal("0.5000") - assert result.order.yes_price == Decimal("0.6000") - assert result.order.order_id == "ord-100" - - @respx.mock - def test_amend_serializes_dollars_and_count(self, orders: OrdersResource) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-200/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-200", ticker="T"), - "order": order_dict(order_id="ord-200", ticker="T"), - }, - ) - ) - orders.amend( - "ord-200", - ticker="T", - side="yes", - action="buy", - yes_price=0.55, - count=20, - ) - - import json - - body = json.loads(route.calls[0].request.content) - assert body["yes_price_dollars"] == "0.55" - assert body["count_fp"] == "20" - assert "yes_price" not in body - assert "count" not in body - - @respx.mock - def test_amend_not_found(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/fake/amend").mock( - return_value=httpx.Response(404, json={"message": "order not found"}) - ) - with pytest.raises(KalshiNotFoundError): - orders.amend("fake", ticker="T", side="yes", action="buy", yes_price=0.50) - - @respx.mock - def test_amend_validation_error(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-300/amend").mock( - return_value=httpx.Response(400, json={"message": "invalid side"}) - ) - with pytest.raises(KalshiValidationError): - orders.amend("ord-300", ticker="T", side="yes", action="buy", yes_price=0.50) - - def test_amend_requires_price_or_count(self, orders: OrdersResource) -> None: - with pytest.raises(ValueError, match="requires at least one"): - orders.amend("ord-123", ticker="T", side="yes", action="buy") - - -class TestOrdersDecrease: - @respx.mock - def test_decrease_by(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-400/decrease").mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-400", ticker="MKT-B", status="resting", remaining_count_fp="5" - ) - }, - ) - ) - order = orders.decrease("ord-400", reduce_by=5) - assert order.order_id == "ord-400" - assert order.remaining_count == Decimal("5") - - @respx.mock - def test_decrease_to(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-500/decrease").mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-500", - ticker="MKT-C", - status="cancelled", - remaining_count_fp="0", - ) - }, - ) - ) - order = orders.decrease("ord-500", reduce_to=0) - assert order.order_id == "ord-500" - assert order.remaining_count == Decimal("0") - - @respx.mock - def test_decrease_not_found(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/fake/decrease").mock( - return_value=httpx.Response(404, json={"message": "order not found"}) - ) - with pytest.raises(KalshiNotFoundError): - orders.decrease("fake", reduce_by=1) - - @respx.mock - def test_decrease_validation_error(self, orders: OrdersResource) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-600/decrease").mock( - return_value=httpx.Response(400, json={"message": "reduce_by must be > 0"}) - ) - with pytest.raises(KalshiValidationError): - orders.decrease("ord-600", reduce_by=-1) - - def test_decrease_requires_reduce_arg(self, orders: OrdersResource) -> None: - with pytest.raises(ValueError, match="requires either reduce_by or reduce_to"): - orders.decrease("ord-123") - - def test_decrease_rejects_both_reduce_args(self, orders: OrdersResource) -> None: - with pytest.raises(ValueError, match="not both"): - orders.decrease("ord-123", reduce_by=5, reduce_to=3) - - class TestOrdersQueuePositions: @respx.mock def test_queue_positions(self, orders: OrdersResource) -> None: @@ -918,18 +387,6 @@ def test_queue_position_not_found(self, orders: OrdersResource) -> None: class TestOrdersAuthGuards: - def test_amend_requires_auth(self, unauth_orders: OrdersResource) -> None: - from kalshi.errors import AuthRequiredError - - with pytest.raises(AuthRequiredError): - unauth_orders.amend("ord-123", ticker="T", side="yes", action="buy") - - def test_decrease_requires_auth(self, unauth_orders: OrdersResource) -> None: - from kalshi.errors import AuthRequiredError - - with pytest.raises(AuthRequiredError): - unauth_orders.decrease("ord-123", reduce_by=1) - def test_queue_positions_requires_auth(self, unauth_orders: OrdersResource) -> None: from kalshi.errors import AuthRequiredError @@ -943,457 +400,6 @@ def test_queue_position_requires_auth(self, unauth_orders: OrdersResource) -> No unauth_orders.queue_position("ord-123") -class TestBatchCancelWireShape: - @respx.mock - def test_wraps_str_ids_into_orders(self, orders: OrdersResource) -> None: - import json - - route = respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - orders.batch_cancel(["ord-1", "ord-2"]) - - body = json.loads(route.calls[0].request.content) - assert "ids" not in body # deprecated field no longer used - assert body["orders"] == [ - {"order_id": "ord-1"}, - {"order_id": "ord-2"}, - ] - - @respx.mock - def test_accepts_typed_order_entries(self, orders: OrdersResource) -> None: - import json - - from kalshi.models.orders import BatchCancelOrdersRequestOrder - - route = respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - orders.batch_cancel( - [ - BatchCancelOrdersRequestOrder(order_id="ord-1", subaccount=5), - BatchCancelOrdersRequestOrder(order_id="ord-2"), - ] - ) - - body = json.loads(route.calls[0].request.content) - assert body["orders"] == [ - {"order_id": "ord-1", "subaccount": 5}, - {"order_id": "ord-2"}, - ] - - -class TestBatchCancelRoutesThroughDeleteWithBodyJson: - """Regression for issue #47 / #223: sync batch_cancel must route through - the shared ``SyncResource._delete_with_body_json`` bytes helper so retry / - error-mapping behavior added to the helper applies to the sync path. - Symmetric with - ``test_async_orders.py::TestAsyncBatchCancelRoutesThroughDeleteWithBodyJson``. - """ - - @respx.mock - def test_batch_cancel_uses_delete_with_body_json_helper( - self, - orders: OrdersResource, - ) -> None: - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - # patch.object + wraps forwards every call to the real method, - # so the respx mock above still resolves; the spy only records. - with patch.object( - orders, - "_delete_with_body_json", - wraps=orders._delete_with_body_json, - ) as spy: - orders.batch_cancel(["ord-1", "ord-2"]) - spy.assert_called_once() - args, kwargs = spy.call_args - assert args == ("/portfolio/orders/batched",) - # Bytes path: caller passes pre-serialized JSON via ``content=``, - # NOT a dict via ``json=`` (P4.2). - assert "json" not in kwargs - assert isinstance(kwargs["content"], bytes) - assert json.loads(kwargs["content"]) == { - "orders": [{"order_id": "ord-1"}, {"order_id": "ord-2"}] - } - - @respx.mock - def test_batch_cancel_returns_typed_response_with_reduced_by_fp( - self, orders: OrdersResource - ) -> None: - """#194: response carries per-leg ``reduced_by_fp`` (Decimal, - load-bearing for risk reconciliation). Previous SDK discarded it. - """ - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order_id": "ord-1", - "reduced_by_fp": "3", - "order": None, - "error": None, - }, - ] - }, - ) - ) - from kalshi.models.orders import BatchCancelOrdersResponse - - result = orders.batch_cancel(["ord-1"]) - assert isinstance(result, BatchCancelOrdersResponse) - assert len(result.orders) == 1 - assert result.orders[0].order_id == "ord-1" - assert result.orders[0].reduced_by_fp == Decimal("3") - assert result.orders[0].error is None - - @respx.mock - def test_batch_cancel_per_entry_error_surfaced(self, orders: OrdersResource) -> None: - """#194: per-leg ``error`` is preserved; ``reduced_by_fp`` is ``0`` - when the leg failed. - """ - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response( - 200, - json={ - "orders": [ - { - "order_id": "ord-bad", - "reduced_by_fp": "0", - "order": None, - "error": {"code": "not_found", "message": "gone"}, - }, - ] - }, - ) - ) - result = orders.batch_cancel(["ord-bad"]) - assert result.orders[0].reduced_by_fp == Decimal("0") - assert result.orders[0].error is not None - assert result.orders[0].error["code"] == "not_found" - - @respx.mock - def test_batch_cancel_raises_on_204_no_content(self, orders: OrdersResource) -> None: - """v3.0.0: typed response required — 204 indicates spec drift.""" - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(204) - ) - with pytest.raises(KalshiError, match="Expected BatchCancelOrdersResponse"): - orders.batch_cancel(["ord-1"]) - - @respx.mock - def test_batch_cancel_200_with_empty_body_raises(self, orders: OrdersResource) -> None: - """Documents an intentional behavior change: the old local helper - discarded the response, so a hypothetical 200 with an empty body - would have been silent. The new shared helper parses JSON on - non-204, so the same case now surfaces as a JSONDecodeError. - Kalshi's spec returns either 200-with-JSON or 204; this test - pins the new failure mode rather than letting it regress silently. - """ - import json as _json - - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200) - ) - - with pytest.raises(_json.JSONDecodeError): - orders.batch_cancel(["ord-1"]) - - -class TestAmendWireShape: - """v0.8.0: orders.amend() builds AmendOrderRequest internally and - serializes via model_dump. Price fields must use _dollars suffix; - count must use count_fp alias; phantom keys must be absent.""" - - @respx.mock - def test_price_serializes_dollars_alias(self, orders: OrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - orders.amend( - "ord-99", - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", - ) - - body = json.loads(route.calls[0].request.content) - assert body["yes_price_dollars"] == "0.55" - assert "yes_price" not in body - - @respx.mock - def test_count_serializes_fp_alias(self, orders: OrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - orders.amend( - "ord-99", - ticker="MKT", - side="yes", - action="buy", - count=3, - ) - - body = json.loads(route.calls[0].request.content) - assert body["count_fp"] == "3" - assert "count" not in body - - @respx.mock - def test_required_and_optional_fields(self, orders: OrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - orders.amend( - "ord-99", - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", - count=3, - subaccount=2, - client_order_id="c-old", - updated_client_order_id="c-new", - ) - - body = json.loads(route.calls[0].request.content) - assert body["ticker"] == "MKT" - assert body["side"] == "yes" - assert body["action"] == "buy" - assert body["yes_price_dollars"] == "0.55" - assert body["count_fp"] == "3" - assert body["subaccount"] == 2 - assert body["client_order_id"] == "c-old" - assert body["updated_client_order_id"] == "c-new" - # no phantom keys - assert "yes_price" not in body - assert "no_price" not in body - assert "count" not in body - - @respx.mock - def test_no_price_absent_when_not_passed(self, orders: OrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/amend" - ).mock( - return_value=httpx.Response( - 200, - json={ - "old_order": order_dict(order_id="ord-99", ticker="MKT"), - "order": order_dict(order_id="ord-99", ticker="MKT"), - }, - ) - ) - - orders.amend( - "ord-99", - ticker="MKT", - side="no", - action="buy", - no_price="0.45", - ) - - body = json.loads(route.calls[0].request.content) - assert body["no_price_dollars"] == "0.45" - assert "no_price" not in body - assert "yes_price_dollars" not in body - assert "count_fp" not in body - - -class TestDecreaseWireShape: - """v0.8.0: orders.decrease() builds DecreaseOrderRequest internally.""" - - @respx.mock - def test_reduce_by_body(self, orders: OrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/decrease" - ).mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-99", ticker="MKT", side="yes", status="resting" - ), - }, - ) - ) - - orders.decrease("ord-99", reduce_by=5, subaccount=1) - - body = json.loads(route.calls[0].request.content) - assert body == {"reduce_by": 5, "subaccount": 1} - - @respx.mock - def test_reduce_to_body(self, orders: OrdersResource) -> None: - import json - - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-99/decrease" - ).mock( - return_value=httpx.Response( - 200, - json={ - "order": order_dict( - order_id="ord-99", ticker="MKT", side="yes", status="resting" - ), - }, - ) - ) - - orders.decrease("ord-99", reduce_to=2) - - body = json.loads(route.calls[0].request.content) - assert body == {"reduce_to": 2} - - -class TestBatchCreateWireShape: - """v0.8.0: orders.batch_create() wraps via BatchCreateOrdersRequest.""" - - @respx.mock - def test_wraps_orders_key(self, orders: OrdersResource) -> None: - import json - - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - orders.batch_create( - [ - CreateOrderRequest(ticker="A", side="yes", action="buy", count=1), - CreateOrderRequest(ticker="B", side="no", action="buy", count=1), - ] - ) - - body = json.loads(route.calls[0].request.content) - assert "orders" in body - assert len(body["orders"]) == 2 - # no phantom top-level keys - assert set(body.keys()) == {"orders"} - - -class TestBatchCreateUsesBytesPath: - """P4.2: batch_create / batch_cancel serialize via model_dump_json directly - to bytes and hand them to httpx as ``content=`` (not ``json=``). This - skips one full dict-walk pass on large payloads where the serializer - cost dominates.""" - - @respx.mock - def test_batch_create_uses_bytes_path(self, orders: OrdersResource) -> None: - """The transport is called with ``content=bytes`` and the - Content-Type header is set explicitly (httpx doesn't infer it for - a raw ``content=`` body).""" - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - with patch.object( - orders._transport, - "request", - wraps=orders._transport.request, - ) as spy: - orders.batch_create([CreateOrderRequest(ticker="A", side="yes", action="buy", count=1)]) - spy.assert_called_once() - args, kwargs = spy.call_args - assert args == ("POST", "/portfolio/orders/batched") - assert "json" not in kwargs or kwargs.get("json") is None - assert isinstance(kwargs["content"], bytes) - assert kwargs["headers"]["Content-Type"] == "application/json" - - @respx.mock - def test_batch_cancel_uses_bytes_path(self, orders: OrdersResource) -> None: - """Mirror of ``test_batch_create_uses_bytes_path`` for batch_cancel - — DELETE-with-body bytes path.""" - respx.delete("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - with patch.object( - orders._transport, - "request", - wraps=orders._transport.request, - ) as spy: - orders.batch_cancel(["ord-1"]) - spy.assert_called_once() - args, kwargs = spy.call_args - assert args == ("DELETE", "/portfolio/orders/batched") - assert "json" not in kwargs or kwargs.get("json") is None - assert isinstance(kwargs["content"], bytes) - assert kwargs["headers"]["Content-Type"] == "application/json" - - @respx.mock - def test_batch_create_bytes_path_preserves_decimal_precision( - self, - orders: OrdersResource, - ) -> None: - """P4.2: the bytes path uses ``model_dump_json`` which round-trips - Decimals via the configured DollarDecimal serializer. Round-trip - ``Decimal("0.5600")`` through the wire and confirm the exact - string is preserved (no float-conversion drift).""" - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - orders.batch_create( - [ - CreateOrderRequest( - ticker="A", - side="yes", - action="buy", - count=10, - yes_price=Decimal("0.5600"), - ), - ] - ) - - raw = route.calls[0].request.content - body = json.loads(raw) - assert body["orders"][0]["yes_price_dollars"] == "0.5600" - - -# ── V2 event-market orders (spec v3.18.0) ─────────────────── - - class TestCreateOrderV2: @respx.mock def test_returns_response(self, orders: OrdersResource) -> None: @@ -1717,6 +723,29 @@ def test_sends_body(self, orders: OrdersResource) -> None: ], } + @respx.mock + def test_auto_route_exchange_index_minus_one(self, orders: OrdersResource) -> None: + """Spec v3.22.0: exchange_index=-1 auto-routes by market_ticker. The + model must accept -1 (no ge=0 floor) and emit market_ticker so the + auto-route contract is usable. + """ + route = respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock(return_value=httpx.Response(200, json={"orders": []})) + orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[ + BatchCancelOrdersV2RequestOrder( + order_id="ord-a", exchange_index=-1, market_ticker="MKT-A", + ), + ], + ), + ) + body = json.loads(route.calls[0].request.content) + assert body == { + "orders": [{"order_id": "ord-a", "exchange_index": -1, "market_ticker": "MKT-A"}], + } + @respx.mock def test_error_entry_parses(self, orders: OrdersResource) -> None: """Per spec, an errored cancel still carries order_id + reduced_by=0 @@ -1961,7 +990,7 @@ def test_empty_string_order_id_raises_value_error(self, orders: OrdersResource) def test_whitespace_only_order_id_rejected(self, orders: OrdersResource) -> None: with pytest.raises(ValueError, match="order_id must be non-empty"): - orders.cancel(" ") + orders.get(" ") def test_dotdot_order_id_rejected(self, orders: OrdersResource) -> None: with pytest.raises(ValueError, match=r"cannot be '\.' or '\.\.'"): @@ -1998,47 +1027,6 @@ def test_fills_rejects_limit_above_1000(self, orders: OrdersResource) -> None: orders.fills(limit=10_000) -class TestCreateOrderRequestLiteralEnforcement: - """#270 Item 2: V1 CreateOrderRequest narrows enum-style fields to Literals. - - Before this change V1 callers could pass ``side="foo"`` and only fail server- - side. V2 already enforces literals at construction; aligning V1 closes the - deprecation-window inconsistency. - """ - - def test_rejects_invalid_side(self) -> None: - with pytest.raises(ValidationError): - CreateOrderRequest(ticker="T", side="foo", action="buy", count=Decimal("1")) # type: ignore[arg-type] - - def test_rejects_invalid_action(self) -> None: - with pytest.raises(ValidationError): - CreateOrderRequest(ticker="T", side="yes", action="trade", count=Decimal("1")) # type: ignore[arg-type] - - def test_rejects_invalid_time_in_force(self) -> None: - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="T", side="yes", action="buy", count=Decimal("1"), - time_in_force="forever", # type: ignore[arg-type] - ) - - def test_rejects_invalid_self_trade_prevention_type(self) -> None: - with pytest.raises(ValidationError): - CreateOrderRequest( - ticker="T", side="yes", action="buy", count=Decimal("1"), - self_trade_prevention_type="never", # type: ignore[arg-type] - ) - - def test_accepts_valid_v1_yes_no_side(self) -> None: - # V1 uses yes/no (NOT bid/ask like V2) - req = CreateOrderRequest(ticker="T", side="no", action="sell", count=Decimal("1")) - assert req.side == "no" - assert req.action == "sell" - - - -# ── Issue #351: deprecated forwarders for fills on OrdersResource ──────────── - - class TestIssue351OrdersFillsDeprecated: @respx.mock def test_issue_351_orders_fills_emits_deprecation_warning( diff --git a/tests/test_request_overload.py b/tests/test_request_overload.py index 88d6c2d..0ca463c 100644 --- a/tests/test_request_overload.py +++ b/tests/test_request_overload.py @@ -25,15 +25,8 @@ CreateMarketInMultivariateEventCollectionRequest, TickerPair, ) -from kalshi.models.orders import ( - AmendOrderRequest, - BatchCreateOrdersRequest, - CreateOrderRequest, -) from kalshi.resources.api_keys import ApiKeysResource from kalshi.resources.multivariate import MultivariateCollectionsResource -from kalshi.resources.orders import OrdersResource -from tests._model_fixtures import order_dict @pytest.fixture @@ -45,11 +38,6 @@ def config() -> KalshiConfig: ) -@pytest.fixture -def orders(test_auth: KalshiAuth, config: KalshiConfig) -> OrdersResource: - return OrdersResource(SyncTransport(test_auth, config)) - - @pytest.fixture def api_keys(test_auth: KalshiAuth, config: KalshiConfig) -> ApiKeysResource: return ApiKeysResource(SyncTransport(test_auth, config)) @@ -63,168 +51,6 @@ def multivariate( return MultivariateCollectionsResource(SyncTransport(test_auth, config)) -_AMEND_RESPONSE = { - "old_order": order_dict(order_id="ord-1", ticker="MKT"), - "order": order_dict(order_id="ord-1", ticker="MKT"), -} - - -class TestAmendRequestOverload: - """`orders.amend` accepts either kwargs or a pre-built AmendOrderRequest.""" - - @respx.mock - def test_request_model_produces_same_body_as_kwargs( - self, - orders: OrdersResource, - ) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-1/amend" - ).mock(return_value=httpx.Response(200, json=_AMEND_RESPONSE)) - - orders.amend( - "ord-1", - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", - count=3, - subaccount=2, - ) - kwarg_body = json.loads(route.calls[0].request.content) - - orders.amend( - "ord-1", - request=AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", - count=3, - subaccount=2, # type: ignore[arg-type] - ), - ) - model_body = json.loads(route.calls[1].request.content) - - assert kwarg_body == model_body - assert kwarg_body["yes_price_dollars"] == "0.55" - assert kwarg_body["count_fp"] == "3" - - @respx.mock - def test_passing_request_and_kwarg_raises( - self, - orders: OrdersResource, - ) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/ord-1/amend").mock( - return_value=httpx.Response(200, json=_AMEND_RESPONSE) - ) - - with pytest.raises(TypeError, match=r"Pass either `request=\.\.\.` or"): - orders.amend( - "ord-1", - request=AmendOrderRequest( - ticker="MKT", - side="yes", - action="buy", - yes_price="0.55", # type: ignore[arg-type] - ), - yes_price="0.60", - ) - - -_ORDER_RESPONSE = {"order": order_dict(order_id="ord-x", ticker="MKT", side="yes")} - - -class TestCreateOrderRequestOverload: - """`orders.create` accepts either kwargs or a pre-built CreateOrderRequest.""" - - @respx.mock - def test_request_model_produces_same_body_as_kwargs( - self, - orders: OrdersResource, - ) -> None: - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json=_ORDER_RESPONSE) - ) - - orders.create( - ticker="MKT", - side="yes", - count=2, - yes_price="0.50", - action="buy", - ) - kwarg_body = json.loads(route.calls[0].request.content) - - orders.create( - request=CreateOrderRequest( - ticker="MKT", - side="yes", - count=2, # type: ignore[arg-type] - yes_price="0.50", # type: ignore[arg-type] - action="buy", - ), - ) - model_body = json.loads(route.calls[1].request.content) - - assert kwarg_body == model_body - - @respx.mock - def test_passing_request_and_kwarg_raises( - self, - orders: OrdersResource, - ) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders").mock( - return_value=httpx.Response(200, json=_ORDER_RESPONSE) - ) - - with pytest.raises(TypeError, match=r"Pass either `request=\.\.\.` or"): - orders.create( - request=CreateOrderRequest(ticker="MKT", side="yes", action="buy", count=1), - ticker="OTHER", - ) - - -class TestBatchCreateRequestOverload: - """`orders.batch_create` (nested model wrapper) accepts request= form.""" - - @respx.mock - def test_request_model_produces_same_body_as_kwargs( - self, - orders: OrdersResource, - ) -> None: - route = respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - inner = [ - CreateOrderRequest(ticker="MKT-A", side="yes", action="buy", count=1), - CreateOrderRequest(ticker="MKT-B", side="no", action="buy", count=1), - ] - orders.batch_create(inner) - kwarg_body = json.loads(route.calls[0].request.content) - - orders.batch_create(request=BatchCreateOrdersRequest(orders=inner)) - model_body = json.loads(route.calls[1].request.content) - - assert kwarg_body == model_body - - @respx.mock - def test_passing_request_and_kwarg_raises( - self, - orders: OrdersResource, - ) -> None: - respx.post("https://test.kalshi.com/trade-api/v2/portfolio/orders/batched").mock( - return_value=httpx.Response(200, json={"orders": []}) - ) - - inner = [CreateOrderRequest(ticker="MKT-A", side="yes", action="buy", count=1)] - with pytest.raises(TypeError, match=r"Pass either `request=\.\.\.` or"): - orders.batch_create( - inner, - request=BatchCreateOrdersRequest(orders=inner), - ) - - class TestCreateApiKeyRequestOverload: """`api_keys.create` accepts request= form (simple POST body, no path params).""" @@ -249,9 +75,7 @@ def test_request_model_produces_same_body_as_kwargs( class TestMissingRequiredKwargsRaisesTypeError: """Calling a dispatcher with no request= and missing required kwargs. - One test per dispatcher shape (single-required, multi-required, - list-required) — locks in the runtime guard mypy's overloads enforce - statically. + Locks in the runtime guard mypy's overloads enforce statically. """ def test_single_required_kwarg_missing( @@ -262,28 +86,6 @@ def test_single_required_kwarg_missing( with pytest.raises(TypeError, match=r"generate\(\) requires `name`"): api_keys.generate() - def test_multi_required_kwargs_missing( - self, - orders: OrdersResource, - ) -> None: - # create() requires `ticker` and `side`. Passing only one raises. - with pytest.raises( - TypeError, - match=r"create\(\) requires `ticker`, `side`, `count`, and `action`", - ): - orders.create(ticker="MKT") - - def test_list_required_kwarg_missing( - self, - orders: OrdersResource, - ) -> None: - # batch_create() requires `orders` (the list). Zero-args raises. - with pytest.raises( - TypeError, - match=r"batch_create\(\) requires `orders`", - ): - orders.batch_create() - class TestCreateMarketRequestOverload: """`multivariate.create_market` — nested-list model + positional path param.""" diff --git a/tests/test_subaccounts.py b/tests/test_subaccounts.py index 2e743ab..a5b6f96 100644 --- a/tests/test_subaccounts.py +++ b/tests/test_subaccounts.py @@ -79,6 +79,7 @@ def test_subaccount_balance_parses_dollar_decimal(self) -> None: bal = SubaccountBalance.model_validate( { "subaccount_number": 1, + "exchange_index": 0, "balance": "12.3400", "updated_ts": 1_700_000_000, } @@ -113,6 +114,7 @@ def test_get_balances_response_wraps_list(self) -> None: "subaccount_balances": [ { "subaccount_number": 0, + "exchange_index": 0, "balance": "100.00", "updated_ts": 1, }, @@ -302,11 +304,13 @@ def test_returns_balances(self, subaccounts: SubaccountsResource) -> None: "subaccount_balances": [ { "subaccount_number": 0, + "exchange_index": 0, "balance": "10.00", "updated_ts": 1, }, { "subaccount_number": 1, + "exchange_index": 0, "balance": "5.00", "updated_ts": 2, }, @@ -500,6 +504,7 @@ async def test_list_balances( "subaccount_balances": [ { "subaccount_number": 0, + "exchange_index": 0, "balance": "1.00", "updated_ts": 1, }, diff --git a/tests/test_types.py b/tests/test_types.py index e81337d..3973688 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -121,12 +121,14 @@ def test_accepts_finite_decimal(self) -> None: m = _DollarModel.model_validate({"x": Decimal("0.5")}) assert m.x == Decimal("0.5") - def test_create_order_request_rejects_nan_yes_price(self) -> None: - from kalshi.models.orders import CreateOrderRequest + def test_create_order_request_rejects_nan_price(self) -> None: + from kalshi.models.orders import CreateOrderV2Request with pytest.raises(ValueError, match="finite"): - CreateOrderRequest( - ticker="T", side="yes", action="buy", - count=Decimal("1"), yes_price=Decimal("NaN"), + CreateOrderV2Request( + ticker="T", client_order_id="c1", side="bid", + count=Decimal("1"), price=Decimal("NaN"), + time_in_force="fill_or_kill", + self_trade_prevention_type="maker", ) diff --git a/tests/ws/test_count_migration.py b/tests/ws/test_count_migration.py index b0f8138..d4e372f 100644 --- a/tests/ws/test_count_migration.py +++ b/tests/ws/test_count_migration.py @@ -4,7 +4,7 @@ from decimal import Decimal -from kalshi.models.orders import CreateOrderRequest, Order +from kalshi.models.orders import CreateOrderV2Request, Order from tests._model_fixtures import order_dict @@ -36,19 +36,26 @@ def test_fill_count_fp_alias(self) -> None: assert order.fill_count == Decimal("15.00") def test_create_order_count_is_decimal(self) -> None: - req = CreateOrderRequest(ticker="ECON-GDP", side="yes", count=Decimal("10"), action="buy") + req = CreateOrderV2Request( + ticker="ECON-GDP", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) assert isinstance(req.count, Decimal) - def test_create_order_count_no_default(self) -> None: - # #242: `count` no longer defaults to Decimal("1") — it is required. - # A missing-arg bug would otherwise silently become a 1-contract BUY. - import pytest - from pydantic import ValidationError - - with pytest.raises(ValidationError): - CreateOrderRequest(ticker="ECON-GDP", side="yes", action="buy") - def test_create_order_count_serializes(self) -> None: - req = CreateOrderRequest(ticker="ECON-GDP", side="yes", count=Decimal("10"), action="buy") + req = CreateOrderV2Request( + ticker="ECON-GDP", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) data = req.model_dump(mode="json") assert isinstance(data["count"], str)